diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 499f5527d2..f9725ddcbc 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -18,15 +18,36 @@
 use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
 
 /**
- * Minimum supported version of PHP.
+ * Minimum allowed version of PHP.
+ *
+ * Below this version:
+ * - The installer cannot be run.
+ * - Updates cannot be run.
+ * - Modules and themes cannot be enabled.
+ * - If a site managed to bypass all of the above, then an error is shown in
+ *   the status report and various fatal errors occur on various pages.
  *
- * Drupal cannot be installed on versions of PHP older than this version.
+ * @see install.php
  *
  * @todo Move this to an appropriate autoloadable class. See
  *   https://www.drupal.org/project/drupal/issues/2908079
  */
 const DRUPAL_MINIMUM_PHP = '5.5.9';
 
+/**
+ * Minimum supported version of PHP.
+ *
+ * Below this version:
+ * - New sites cannot be installed, except from within tests.
+ * - Updates from previous Drupal versions can be run, but users are warned
+ *   that Drupal no longer supports this PHP version.
+ * - An error is shown in the status report that the PHP version is too old.
+ *
+ * @todo Move this to an appropriate autoloadable class. See
+ *   https://www.drupal.org/project/drupal/issues/2908079
+ */
+const DRUPAL_MINIMUM_SUPPORTED_PHP = '7.0.8';
+
 /**
  * Minimum recommended version of PHP.
  *
diff --git a/core/modules/locale/tests/src/Functional/LocaleTranslatedSchemaDefinitionTest.php b/core/modules/locale/tests/src/Functional/LocaleTranslatedSchemaDefinitionTest.php
index 3e39f74626..ba5afb933e 100644
--- a/core/modules/locale/tests/src/Functional/LocaleTranslatedSchemaDefinitionTest.php
+++ b/core/modules/locale/tests/src/Functional/LocaleTranslatedSchemaDefinitionTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\RequirementsPageTrait;
 
 /**
  * Adds and configures languages to check field schema definition.
@@ -12,6 +13,8 @@
  */
 class LocaleTranslatedSchemaDefinitionTest extends BrowserTestBase {
 
+  use RequirementsPageTrait;
+
   /**
    * Modules to enable.
    *
@@ -83,6 +86,8 @@ public function testTranslatedUpdate() {
     // markup and a link instead of specific text because text may be
     // translated.
     $this->drupalGet($update_url . '/selection', ['external' => TRUE]);
+    $this->updateRequirementsProblem();
+    $this->drupalGet($update_url . '/selection', ['external' => TRUE]);
     $this->assertRaw('messages--status', 'No pending updates.');
     $this->assertNoLinkByHref('fr/update.php/run', 'No link to run updates.');
   }
diff --git a/core/modules/simpletest/src/InstallerTestBase.php b/core/modules/simpletest/src/InstallerTestBase.php
index 369c3ab1ac..0f4af3a3a7 100644
--- a/core/modules/simpletest/src/InstallerTestBase.php
+++ b/core/modules/simpletest/src/InstallerTestBase.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Language\Language;
 use Drupal\Core\Session\UserSession;
 use Drupal\Core\Site\Settings;
+use Drupal\Tests\RequirementsPageTrait;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\DependencyInjection\Reference;
 use Symfony\Component\HttpFoundation\Request;
@@ -22,6 +23,8 @@
  */
 abstract class InstallerTestBase extends WebTestBase {
 
+  use RequirementsPageTrait;
+
   /**
    * Custom settings.php values to write for a test run.
    *
@@ -213,12 +216,7 @@ protected function setUpSettings() {
    * @see system_requirements()
    */
   protected function setUpRequirementsProblem() {
-    // By default, skip the "recommended PHP version" warning on older test
-    // environments. This allows the installer to be tested consistently on
-    // both recommended PHP versions and older (but still supported) versions.
-    if (version_compare(phpversion(), '7.0') < 0) {
-      $this->continueOnExpectedWarnings(['PHP']);
-    }
+    // Do nothing.
   }
 
   /**
@@ -244,44 +242,4 @@ protected function refreshVariables() {
     }
   }
 
-  /**
-   * Continues installation when an expected warning is found.
-   *
-   * @param string[] $expected_warnings
-   *   A list of warning summaries to expect on the requirements screen (e.g.
-   *   'PHP', 'PHP OPcode caching', etc.). If only the expected warnings
-   *   are found, the test will click the "continue anyway" link to go to the
-   *   next screen of the installer. If an expected warning is not found, or if
-   *   a warning not in the list is present, a fail is raised.
-   */
-  protected function continueOnExpectedWarnings($expected_warnings = []) {
-    // Don't try to continue if there are errors.
-    if (strpos($this->getTextContent(), 'Errors found') !== FALSE) {
-      return;
-    }
-    // Allow only details elements that are directly after the warning header
-    // or each other. There is no guaranteed wrapper we can rely on across
-    // distributions. When there are multiple warnings, the selectors will be:
-    // - h3#warning+details summary
-    // - h3#warning+details+details summary
-    // - etc.
-    // We add one more selector than expected warnings to confirm that there
-    // isn't any other warning before clicking the link.
-    // @todo Make this more reliable in
-    //   https://www.drupal.org/project/drupal/issues/2927345.
-    $selectors = [];
-    for ($i = 0; $i <= count($expected_warnings); $i++) {
-      $selectors[] = 'h3#warning' . implode('', array_fill(0, $i + 1, '+details')) . ' summary';
-    }
-    $warning_elements = $this->cssSelect(implode(', ', $selectors));
-
-    // Confirm that there are only the expected warnings.
-    $warnings = [];
-    foreach ($warning_elements as $warning) {
-      $warnings[] = trim((string) $warning);
-    }
-    $this->assertEqual($expected_warnings, $warnings);
-    $this->clickLink('continue anyway');
-  }
-
 }
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index d810a24c78..7cff855afa 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -186,25 +186,43 @@ function system_requirements($phase) {
     ];
   }
 
-  if (version_compare($phpversion, DRUPAL_MINIMUM_PHP) < 0) {
-    $requirements['php']['description'] = t('Your PHP installation is too old. Drupal requires at least PHP %version.', ['%version' => DRUPAL_MINIMUM_PHP]);
+  // Check if the PHP version is below what Drupal supports.
+  if (version_compare($phpversion, DRUPAL_MINIMUM_SUPPORTED_PHP) < 0) {
+    $requirements['php']['description'] = t('Your PHP installation is too old. Drupal requires at least PHP %version. It is recommended to upgrade to PHP version %recommended or higher for the best ongoing support. See <a href="http://php.net/supported-versions.php">PHP\'s version support documentation</a> and the <a href=":php_requirements">Drupal 8 PHP requirements handbook page</a> for more information.',
+      [
+        '%version' => DRUPAL_MINIMUM_SUPPORTED_PHP,
+        '%recommended' => DRUPAL_RECOMMENDED_PHP,
+        ':php_requirements' => 'https://www.drupal.org/docs/8/system-requirements/php',
+      ]
+    );
     $requirements['php']['severity'] = REQUIREMENT_ERROR;
-    // If PHP is old, it's not safe to continue with the requirements check.
-    return $requirements;
-  }
-  if ((version_compare($phpversion, DRUPAL_RECOMMENDED_PHP) < 0) && ($phase === 'install' || $phase === 'runtime')) {
-    // Warn if still on PHP 5. If at least PHP 7.0, relax from "warning" to
-    // "info", and show it at runtime only, to not scare users while installing.
-    if (version_compare($phpversion, '7.0') < 0) {
-      $requirements['php']['description'] = t('Drupal will drop support for this version on March 6, 2019. Upgrade to PHP version %recommended or higher to ensure your site can receive updates and remain secure. See <a href="http://php.net/supported-versions.php">PHP\'s version support documentation</a> and the <a href=":php_requirements">Drupal 8 PHP requirements handbook page</a> for more information.', ['%recommended' => DRUPAL_RECOMMENDED_PHP, ':php_requirements' => 'https://www.drupal.org/docs/8/system-requirements/php']);
-      $requirements['php']['severity'] = REQUIREMENT_WARNING;
+
+    // If the PHP version is also below the absolute minimum allowed, it's not
+    // safe to continue with the requirements check.
+    if (version_compare($phpversion, DRUPAL_MINIMUM_PHP) < 0) {
+      return $requirements;
     }
-    else {
-      if ($phase === 'runtime') {
-        $requirements['php']['description'] = t('It is recommended to upgrade to PHP version %recommended or higher for the best ongoing support.  See <a href="http://php.net/supported-versions.php">PHP\'s version support documentation</a> and the <a href=":php_requirements">Drupal 8 PHP requirements handbook page</a> for more information.', ['%recommended' => DRUPAL_RECOMMENDED_PHP, ':php_requirements' => 'https://www.drupal.org/docs/8/system-requirements/php']);
-        $requirements['php']['severity'] = REQUIREMENT_INFO;
-      }
+    // Otherwise downgrade the error to a warning during updates. Even if there
+    // are some problems with the site's PHP version, it's still better for the
+    // site to keep its Drupal codebase up to date.
+    elseif ($phase === 'update') {
+      $requirements['php']['severity'] = REQUIREMENT_WARNING;
     }
+    // Since we allow sites with unsupported PHP versions to still run Drupal
+    // updates, we also need to be able to run tests with those PHP versions,
+    // which requires the ability to install test sites. Not all tests are
+    // required to pass on these PHP versions, but we want to monitor which
+    // ones do and don't.
+    elseif ($phase === 'install' && drupal_valid_test_ua()) {
+      $requirements['php']['severity'] = REQUIREMENT_INFO;
+    }
+  }
+  // For PHP versions that are still supported but no longer recommended,
+  // inform users of what's recommended, allowing them to take action before it
+  // becomes urgent.
+  elseif ($phase === 'runtime' && version_compare($phpversion, DRUPAL_RECOMMENDED_PHP) < 0) {
+    $requirements['php']['description'] = t('It is recommended to upgrade to PHP version %recommended or higher for the best ongoing support.  See <a href="http://php.net/supported-versions.php">PHP\'s version support documentation</a> and the <a href=":php_requirements">Drupal 8 PHP requirements handbook page</a> for more information.', ['%recommended' => DRUPAL_RECOMMENDED_PHP, ':php_requirements' => 'https://www.drupal.org/docs/8/system-requirements/php']);
+    $requirements['php']['severity'] = REQUIREMENT_INFO;
   }
 
   // Suggest to update to at least 5.5.21 or 5.6.5 for disabling multiple
diff --git a/core/modules/system/tests/src/Functional/Update/DbUpdatesTrait.php b/core/modules/system/tests/src/Functional/Update/DbUpdatesTrait.php
index 55f3a04a5a..5fbc1ee181 100644
--- a/core/modules/system/tests/src/Functional/Update/DbUpdatesTrait.php
+++ b/core/modules/system/tests/src/Functional/Update/DbUpdatesTrait.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Url;
+use Drupal\Tests\RequirementsPageTrait;
 
 /**
  * Provides methods to conditionally enable db update functions and apply
@@ -14,6 +15,7 @@
 trait DbUpdatesTrait {
 
   use StringTranslationTrait;
+  use RequirementsPageTrait;
 
   /**
    * Enables db updates until the specified index.
@@ -34,6 +36,7 @@ protected function enableUpdates($module, $group, $index) {
    */
   protected function applyUpdates() {
     $this->drupalGet(Url::fromRoute('system.db_update'));
+    $this->updateRequirementsProblem();
     $this->clickLink($this->t('Continue'));
     $this->clickLink($this->t('Apply pending updates'));
     $this->checkForMetaRefresh();
diff --git a/core/modules/system/tests/src/Functional/Update/InvalidUpdateHookTest.php b/core/modules/system/tests/src/Functional/Update/InvalidUpdateHookTest.php
index 85d041e2a4..44fdf81ab9 100644
--- a/core/modules/system/tests/src/Functional/Update/InvalidUpdateHookTest.php
+++ b/core/modules/system/tests/src/Functional/Update/InvalidUpdateHookTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\system\Functional\Update;
 
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\RequirementsPageTrait;
 
 /**
  * Tests that a module implementing hook_update_8000() causes an error to be
@@ -12,6 +13,8 @@
  */
 class InvalidUpdateHookTest extends BrowserTestBase {
 
+  use RequirementsPageTrait;
+
   /**
    * Modules to enable.
    *
@@ -45,6 +48,7 @@ public function testInvalidUpdateHook() {
     // Confirm that a module with hook_update_8000() cannot be updated.
     $this->drupalLogin($this->updateUser);
     $this->drupalGet($this->updateUrl);
+    $this->updateRequirementsProblem();
     $this->clickLink(t('Continue'));
     $this->assertText(t('Some of the pending updates cannot be applied because their dependencies were not met.'));
   }
diff --git a/core/modules/system/tests/src/Functional/Update/UpdatePostUpdateTest.php b/core/modules/system/tests/src/Functional/Update/UpdatePostUpdateTest.php
index fd9e011700..3e2c46a99c 100644
--- a/core/modules/system/tests/src/Functional/Update/UpdatePostUpdateTest.php
+++ b/core/modules/system/tests/src/Functional/Update/UpdatePostUpdateTest.php
@@ -77,6 +77,8 @@ public function testPostUpdate() {
       $this->assertEqual($existing_updates[$expected_update], 1, new FormattableMarkup("@expected_update exists in 'existing_updates' key and only appears once.", ['@expected_update' => $expected_update]));
     }
 
+    $this->drupalGet('update.php/selection');
+    $this->updateRequirementsProblem();
     $this->drupalGet('update.php/selection');
     $this->assertText('No pending updates.');
   }
diff --git a/core/modules/system/tests/src/Functional/Update/UpdateSchemaTest.php b/core/modules/system/tests/src/Functional/Update/UpdateSchemaTest.php
index e35d52b04c..aa28be020d 100644
--- a/core/modules/system/tests/src/Functional/Update/UpdateSchemaTest.php
+++ b/core/modules/system/tests/src/Functional/Update/UpdateSchemaTest.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Database\Database;
 use Drupal\Core\Url;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\RequirementsPageTrait;
 
 /**
  * Tests that update hooks are properly run.
@@ -13,6 +14,8 @@
  */
 class UpdateSchemaTest extends BrowserTestBase {
 
+  use RequirementsPageTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -56,6 +59,7 @@ public function testUpdateHooks() {
 
     $this->drupalLogin($this->user);
     $this->drupalGet($this->updateUrl, ['external' => TRUE]);
+    $this->updateRequirementsProblem();
     $this->clickLink(t('Continue'));
     $this->assertRaw('Schema version 8001.');
     // Run the update hooks.
diff --git a/core/modules/system/tests/src/Functional/Update/UpdateScriptTest.php b/core/modules/system/tests/src/Functional/Update/UpdateScriptTest.php
index da215d4c7d..162cbf482c 100644
--- a/core/modules/system/tests/src/Functional/Update/UpdateScriptTest.php
+++ b/core/modules/system/tests/src/Functional/Update/UpdateScriptTest.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Url;
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\RequirementsPageTrait;
 
 /**
  * Tests the update script access and functionality.
@@ -13,6 +14,8 @@
  */
 class UpdateScriptTest extends BrowserTestBase {
 
+  use RequirementsPageTrait;
+
   /**
    * Modules to enable.
    *
@@ -99,6 +102,7 @@ public function testRequirements() {
     // If there are no requirements warnings or errors, we expect to be able to
     // go through the update process uninterrupted.
     $this->drupalGet($this->updateUrl, ['external' => TRUE]);
+    $this->updateRequirementsProblem();
     $this->clickLink(t('Continue'));
     $this->assertText(t('No pending updates.'), 'End of update process was reached.');
     // Confirm that all caches were cleared.
@@ -110,6 +114,7 @@ public function testRequirements() {
 
     // First, run this test with pending updates to make sure they can be run
     // successfully.
+    $this->drupalLogin($this->updateUser);
     $update_script_test_config->set('requirement_type', REQUIREMENT_WARNING)->save();
     drupal_set_installed_schema_version('update_script_test', drupal_get_installed_schema_version('update_script_test') - 1);
     $this->drupalGet($this->updateUrl, ['external' => TRUE]);
@@ -177,6 +182,7 @@ public function testNoUpdateFunctionality() {
     // Click through update.php with 'administer software updates' permission.
     $this->drupalLogin($this->updateUser);
     $this->drupalGet($this->updateUrl, ['external' => TRUE]);
+    $this->updateRequirementsProblem();
     $this->clickLink(t('Continue'));
     $this->assertText(t('No pending updates.'));
     $this->assertNoLink('Administration pages');
@@ -188,6 +194,7 @@ public function testNoUpdateFunctionality() {
     $admin_user = $this->drupalCreateUser(['administer software updates', 'access administration pages']);
     $this->drupalLogin($admin_user);
     $this->drupalGet($this->updateUrl, ['external' => TRUE]);
+    $this->updateRequirementsProblem();
     $this->clickLink(t('Continue'));
     $this->assertText(t('No pending updates.'));
     $this->assertLink('Administration pages');
@@ -220,6 +227,7 @@ public function testSuccessfulUpdateFunctionality() {
     $admin_user = $this->drupalCreateUser(['administer software updates', 'access administration pages', 'access site reports', 'access site in maintenance mode']);
     $this->drupalLogin($admin_user);
     $this->drupalGet($this->updateUrl, ['external' => TRUE]);
+    $this->updateRequirementsProblem();
     $this->clickLink(t('Continue'));
     $this->clickLink(t('Apply pending updates'));
     $this->checkForMetaRefresh();
@@ -287,6 +295,7 @@ public function testSuccessfulMultilingualUpdateFunctionality() {
     // Click through update.php with 'access administration pages' and
     // 'access site reports' permissions.
     $this->drupalGet($this->updateUrl, ['external' => TRUE]);
+    $this->updateRequirementsProblem();
     $this->clickLink(t('Continue'));
     $this->clickLink(t('Apply pending updates'));
     $this->checkForMetaRefresh();
@@ -319,6 +328,7 @@ protected function runUpdates($maintenance_mode) {
       $this->assertNoText('Operating in maintenance mode.');
     }
     $this->drupalGet($this->updateUrl, ['external' => TRUE]);
+    $this->updateRequirementsProblem();
     $this->clickLink(t('Continue'));
     $this->clickLink(t('Apply pending updates'));
     $this->checkForMetaRefresh();
diff --git a/core/modules/system/tests/src/Functional/Update/UpdatesWith7xTest.php b/core/modules/system/tests/src/Functional/Update/UpdatesWith7xTest.php
index 4f536ea5aa..7d3a5ef3ed 100644
--- a/core/modules/system/tests/src/Functional/Update/UpdatesWith7xTest.php
+++ b/core/modules/system/tests/src/Functional/Update/UpdatesWith7xTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\system\Functional\Update;
 
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\RequirementsPageTrait;
 
 /**
  * Tests that the minimum schema version is correct even if only 7.x update
@@ -12,6 +13,8 @@
  */
 class UpdatesWith7xTest extends BrowserTestBase {
 
+  use RequirementsPageTrait;
+
   /**
    * Modules to enable.
    *
@@ -48,6 +51,7 @@ public function testWith7x() {
     // Click through update.php with 'administer software updates' permission.
     $this->drupalLogin($this->updateUser);
     $this->drupalGet($this->updateUrl, ['external' => TRUE]);
+    $this->updateRequirementsProblem();
     $this->clickLink(t('Continue'));
     $this->assertText(t('Some of the pending updates cannot be applied because their dependencies were not met.'));
   }
diff --git a/core/profiles/minimal/tests/src/Functional/MinimalTest.php b/core/profiles/minimal/tests/src/Functional/MinimalTest.php
index 568918a954..d743fee5f4 100644
--- a/core/profiles/minimal/tests/src/Functional/MinimalTest.php
+++ b/core/profiles/minimal/tests/src/Functional/MinimalTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\minimal\Functional;
 
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\RequirementsPageTrait;
 use Drupal\user\UserInterface;
 
 /**
@@ -12,6 +13,8 @@
  */
 class MinimalTest extends BrowserTestBase {
 
+  use RequirementsPageTrait;
+
   protected $profile = 'minimal';
 
   /**
@@ -34,6 +37,8 @@ public function testMinimal() {
     // Ensure that there are no pending updates after installation.
     $this->drupalLogin($this->rootUser);
     $this->drupalGet('update.php/selection');
+    $this->updateRequirementsProblem();
+    $this->drupalGet('update.php/selection');
     $this->assertText('No pending updates.');
 
     // Ensure that there are no pending entity updates after installation.
diff --git a/core/profiles/standard/tests/src/Functional/StandardTest.php b/core/profiles/standard/tests/src/Functional/StandardTest.php
index 79584b4420..f62e183fca 100644
--- a/core/profiles/standard/tests/src/Functional/StandardTest.php
+++ b/core/profiles/standard/tests/src/Functional/StandardTest.php
@@ -10,6 +10,7 @@
 use Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber;
 use Drupal\filter\Entity\FilterFormat;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\RequirementsPageTrait;
 use Drupal\user\Entity\Role;
 
 /**
@@ -20,6 +21,7 @@
 class StandardTest extends BrowserTestBase {
 
   use SchemaCheckTestTrait;
+  use RequirementsPageTrait;
 
   protected $profile = 'standard';
 
@@ -155,6 +157,8 @@ public function testStandard() {
     // Ensure that there are no pending updates after installation.
     $this->drupalLogin($this->rootUser);
     $this->drupalGet('update.php/selection');
+    $this->updateRequirementsProblem();
+    $this->drupalGet('update.php/selection');
     $this->assertText('No pending updates.');
 
     // Ensure that there are no pending entity updates after installation.
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php
index 9810f3425c..a66bd5759e 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Site\Settings;
 use Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\RequirementsPageTrait;
 use GuzzleHttp\HandlerStack;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\DependencyInjection\Reference;
@@ -19,6 +20,8 @@
  */
 abstract class InstallerTestBase extends BrowserTestBase {
 
+  use RequirementsPageTrait;
+
   /**
    * Custom settings.php values to write for a test run.
    *
@@ -244,12 +247,7 @@ protected function setUpSettings() {
    * @see system_requirements()
    */
   protected function setUpRequirementsProblem() {
-    // By default, skip the "recommended PHP version" warning on older test
-    // environments. This allows the installer to be tested consistently on
-    // both recommended PHP versions and older (but still supported) versions.
-    if (version_compare(phpversion(), '7.0') < 0) {
-      $this->continueOnExpectedWarnings(['PHP']);
-    }
+    // Do nothing.
   }
 
   /**
@@ -275,45 +273,4 @@ protected function refreshVariables() {
     }
   }
 
-  /**
-   * Continues installation when an expected warning is found.
-   *
-   * @param string[] $expected_warnings
-   *   A list of warning summaries to expect on the requirements screen (e.g.
-   *   'PHP', 'PHP OPcode caching', etc.). If only the expected warnings
-   *   are found, the test will click the "continue anyway" link to go to the
-   *   next screen of the installer. If an expected warning is not found, or if
-   *   a warning not in the list is present, a fail is raised.
-   */
-  protected function continueOnExpectedWarnings($expected_warnings = []) {
-    // Don't try to continue if there are errors.
-    if (strpos($this->getTextContent(), 'Errors found') !== FALSE) {
-      return;
-    }
-    // Allow only details elements that are directly after the warning header
-    // or each other. There is no guaranteed wrapper we can rely on across
-    // distributions. When there are multiple warnings, the selectors will be:
-    // - h3#warning+details summary
-    // - h3#warning+details+details summary
-    // - etc.
-    // We add one more selector than expected warnings to confirm that there
-    // isn't any other warning before clicking the link.
-    // @todo Make this more reliable in
-    //   https://www.drupal.org/project/drupal/issues/2927345.
-    $selectors = [];
-    for ($i = 0; $i <= count($expected_warnings); $i++) {
-      $selectors[] = 'h3#warning' . implode('', array_fill(0, $i + 1, '+details')) . ' summary';
-    }
-    $warning_elements = $this->cssSelect(implode(', ', $selectors));
-
-    // Confirm that there are only the expected warnings.
-    $warnings = [];
-    foreach ($warning_elements as $warning) {
-      $warnings[] = trim($warning->getText());
-    }
-    $this->assertEquals($expected_warnings, $warnings);
-    $this->clickLink('continue anyway');
-    $this->checkForMetaRefresh();
-  }
-
 }
diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
index a127ab4833..af27c7de57 100644
--- a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
@@ -10,6 +10,7 @@
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Url;
+use Drupal\Tests\RequirementsPageTrait;
 use Drupal\user\Entity\User;
 use Symfony\Component\DependencyInjection\Reference;
 use Symfony\Component\HttpFoundation\Request;
@@ -42,6 +43,7 @@
 abstract class UpdatePathTestBase extends BrowserTestBase {
 
   use SchemaCheckTestTrait;
+  use RequirementsPageTrait;
 
   /**
    * Modules to enable after the database is loaded.
@@ -295,6 +297,7 @@ protected function runUpdates() {
     ]);
 
     $this->drupalGet($this->updateUrl);
+    $this->updateRequirementsProblem();
     $this->clickLink(t('Continue'));
 
     $this->doSelectionTest();
diff --git a/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php b/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php
index 5622aa2375..830eb0d5ce 100644
--- a/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php
+++ b/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php
@@ -58,6 +58,7 @@ public function setUp() {
     }
     // Get a lock and a valid site path.
     $this->testDb = new TestDatabase();
+    include $this->root . '/core/includes/bootstrap.inc';
   }
 
   /**
@@ -83,6 +84,10 @@ public function tearDown() {
    * Tests the quick-start command.
    */
   public function testQuickStartCommand() {
+    if (version_compare(phpversion(), DRUPAL_MINIMUM_SUPPORTED_PHP) < 0) {
+      $this->markTestSkipped();
+    }
+
     // Install a site using the standard profile to ensure the one time login
     // link generation works.
 
@@ -117,7 +122,6 @@ public function testQuickStartCommand() {
     $this->assertContains("127.0.0.1:$port/user/reset/1/", $process->getOutput());
 
     // Generate a cookie so we can make a request against the installed site.
-    include $this->root . '/core/includes/bootstrap.inc';
     define('DRUPAL_TEST_IN_CHILD_SITE', FALSE);
     chmod($this->testDb->getTestSitePath(), 0755);
     $cookieJar = CookieJar::fromArray([
@@ -132,10 +136,48 @@ public function testQuickStartCommand() {
     $process->stop();
   }
 
+  /**
+   * Tests that the installer throws a requirement error on older PHP versions.
+   */
+  public function testPhpRequirement() {
+    if (version_compare(phpversion(), DRUPAL_MINIMUM_SUPPORTED_PHP) >= 0) {
+      $this->markTestSkipped();
+    }
+
+    $install_command = [
+      $this->php,
+      'core/scripts/drupal',
+      'quick-start',
+      'standard',
+      "--site-name='Test site {$this->testDb->getDatabasePrefix()}'",
+      '--suppress-login',
+    ];
+    $process = new Process($install_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]);
+    $process->inheritEnvironmentVariables();
+    $process->setTimeout(500);
+    $process->start();
+    while ($process->isRunning()) {
+      // Wait for more output.
+      sleep(1);
+    }
+
+    $error_output = $process->getErrorOutput();
+    $this->assertContains('Your PHP installation is too old.', $error_output);
+    $this->assertContains('Drupal requires at least PHP', $error_output);
+    $this->assertContains(DRUPAL_MINIMUM_SUPPORTED_PHP, $error_output);
+
+    // Stop the web server.
+    $process->stop();
+  }
+
   /**
    * Tests the quick-start commands.
    */
   public function testQuickStartInstallAndServerCommands() {
+    if (version_compare(phpversion(), DRUPAL_MINIMUM_SUPPORTED_PHP) < 0) {
+      $this->markTestSkipped();
+    }
+
     // Install a site.
     $install_command = [
       $this->php,
@@ -180,7 +222,6 @@ public function testQuickStartInstallAndServerCommands() {
     sleep(2);
 
     // Generate a cookie so we can make a request against the installed site.
-    include $this->root . '/core/includes/bootstrap.inc';
     define('DRUPAL_TEST_IN_CHILD_SITE', FALSE);
     chmod($this->testDb->getTestSitePath(), 0755);
     $cookieJar = CookieJar::fromArray([
diff --git a/core/tests/Drupal/Tests/RequirementsPageTrait.php b/core/tests/Drupal/Tests/RequirementsPageTrait.php
new file mode 100644
index 0000000000..d77a209d95
--- /dev/null
+++ b/core/tests/Drupal/Tests/RequirementsPageTrait.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\Tests;
+
+/**
+ * Provides helper methods for the requirements page.
+ */
+trait RequirementsPageTrait {
+
+  /**
+   * Handles the update requirements page.
+   */
+  protected function updateRequirementsProblem() {
+    // Assert a warning is shown on older test environments.
+    if (version_compare(phpversion(), DRUPAL_MINIMUM_SUPPORTED_PHP) < 0) {
+      $this->assertNoText('Errors found');
+      $this->assertWarningSummaries(['PHP']);
+      $this->clickLink('try again');
+      $this->checkForMetaRefresh();
+    }
+  }
+
+  /**
+   * Continues installation when the expected warnings are found.
+   *
+   * @param string[] $expected_warnings
+   *   A list of warning summaries to expect on the requirements screen (e.g.
+   *   'PHP', 'PHP OPcode caching', etc.). If only the expected warnings
+   *   are found, the test will click the "continue anyway" link to go to the
+   *   next screen of the installer. If an expected warning is not found, or if
+   *   a warning not in the list is present, a fail is raised.
+   */
+  protected function continueOnExpectedWarnings($expected_warnings = []) {
+    $this->assertNoText('Errors found');
+    $this->assertWarningSummaries($expected_warnings);
+    $this->clickLink('continue anyway');
+    $this->checkForMetaRefresh();
+  }
+
+  /**
+   * Assert the given warning summaries are present on the page.
+   *
+   * If an expected warning is not found, or if a warning not in the list is
+   * present, a fail is raised.
+   *
+   * @param string[] $warning_summaries
+   *   A list of warning summaries to expect on the requirements screen (e.g.
+   *   'PHP', 'PHP OPcode caching', etc.).
+   */
+  protected function assertWarningSummaries(array $warning_summaries) {
+    // Allow only details elements that are directly after the warning header
+    // or each other. There is no guaranteed wrapper we can rely on across
+    // distributions. When there are multiple warnings, the selectors will be:
+    // - h3#warning+details summary
+    // - h3#warning+details+details summary
+    // - etc.
+    // We add one more selector than expected warnings to confirm that there
+    // isn't any other warning before clicking the link.
+    // @todo Make this more reliable in
+    //   https://www.drupal.org/project/drupal/issues/2927345.
+    $selectors = [];
+    for ($i = 0; $i <= count($warning_summaries); $i++) {
+      $selectors[] = 'h3#warning' . implode('', array_fill(0, $i + 1, '+details')) . ' summary';
+    }
+    $warning_elements = $this->cssSelect(implode(', ', $selectors));
+
+    // Confirm that there are only the expected warnings.
+    $warnings = [];
+    foreach ($warning_elements as $warning) {
+      $warnings[] = trim($warning->getText());
+    }
+    $this->assertEquals($warning_summaries, $warnings);
+  }
+
+}
