Issue #3261447 by xjm, lauriii, ravi.shankar, Wim Leers, alexpott, tedbow, daffie: Add an API for dynamically setting recommended and supported PHP versions based on known and predicted PHP release schedules

merge-requests/7755/head
Alex Pott 2022-05-26 19:10:48 +01:00
parent a40e8436a2
commit 3412b7bdb8
No known key found for this signature in database
GPG Key ID: BDA67E7EE836E5CE
8 changed files with 315 additions and 23 deletions

View File

@ -87,17 +87,6 @@ class Drupal {
*/
const CORE_MINIMUM_SCHEMA_VERSION = 8000;
/**
* 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 that PHP version.
* - An error is shown in the status report that the PHP version is too old.
*/
const MINIMUM_SUPPORTED_PHP = '8.1.0';
/**
* Minimum allowed version of PHP for Drupal to be bootstrapped.
*

View File

@ -0,0 +1,109 @@
<?php
namespace Drupal\Core\Utility;
/**
* Provides an object for dynamically identifying the minimum supported PHP.
*/
final class PhpRequirements {
/**
* The minimum PHP version requirement for the installed Drupal version.
*
* This property is maintained to make the class testable.
*
* @var string
*
* @see version_compare()
*/
private static $drupalMinimumPhp = \Drupal::MINIMUM_PHP;
/**
* The expected PHP version end-of-life dates, keyed by PHP minor version.
*
* The array keys are in 'major.minor' format, and the date values are in ISO
* 8601 format.
*
* @var string[]
* An array of end-of-life dates in ISO 8601 format, keyed by the PHP minor
* version in 'major.minor' format. The list must be sorted in an ascending
* order by the date. Multiple versions EOL on the same day must be sorted
* by the PHP version.
*/
private static $phpEolDates = [
'7.3' => '2021-12-06',
'7.4' => '2022-11-28',
'8.0' => '2023-11-26',
'8.1' => '2024-11-25',
];
/**
* This class should not be instantiated.
*/
private function __construct() {
}
/**
* Dynamically identifies the minimum supported PHP version based on the date.
*
* Drupal automatically increases the minimum supported PHP version from
* \Drupal::MINIMUM_PHP to a newer version after PHP's documented end-of-life
* date for the previous version.
*
* Below this version:
* - New sites can be installed (to allow update deployment workflows that
* reinstall sites from configuration), but a warning is displayed in the
* installer that the PHP version is too old (except within tests).
* - Updates from previous Drupal versions can be run, but users are warned
* that Drupal no longer supports that PHP version.
* - An error is shown in the status report that the PHP version is too old.
*
* @param \DateTime|null $date
* The DateTime to check. Defaults to the current datetime (now) if NULL.
*
* @return string
* The minimum supported PHP version on the date in a PHP-standardized
* number format supported by version_compare(). For example, '8.0.2' or
* '8.1'. This will be the lowest PHP version above the minimum PHP version
* supported by Drupal that is still supported, or the highest known PHP
* version if no known versions are still supported.
*
* @see version_compare()
*/
public static function getMinimumSupportedPhp(?\DateTime $date = NULL): string {
// By default, use the current date (right now).
$date = $date ?? new \DateTime('now');
// In case no data are available or all known PHP versions in this class
// are already end-of-life, default to the version that had the most recent
// end-of-life (the key of the last element in the sorted array).
// The string cast ensures the value is a string, even if the PHP EOL date
// array is empty. As of PHP 8.1, version_compare() no longer accepts NULL
// as a parameter; empty string must be used instead.
$lowest_supported_version = $lowest_supported_version ?? (string) array_key_last(static::$phpEolDates);
// Next, look at versions that are end-of-life after the current date.
// Find the lowest PHP version that is still supported.
foreach (static::$phpEolDates as $version => $eol_date) {
$eol_datetime = new \DateTime($eol_date);
if ($eol_datetime > $date) {
// If $version is less than the previously discovered lowest supported
// version, use $version as the lowest supported version instead.
if (version_compare($version, $lowest_supported_version) < 0) {
$lowest_supported_version = $version;
}
}
}
// If PHP versions older than the Drupal minimum PHP version are still
// supported, return Drupal minimum PHP version instead.
if (version_compare($lowest_supported_version, static::$drupalMinimumPhp) < 0) {
return static::$drupalMinimumPhp;
}
// Otherwise, return the lowest supported PHP version.
return $lowest_supported_version;
}
}

View File

@ -16,6 +16,7 @@ use Drupal\Core\DrupalKernel;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Link;
use Drupal\Core\Utility\PhpRequirements;
use Drupal\Core\Render\Markup;
use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\PrivateStream;
@ -31,6 +32,10 @@ use Symfony\Component\HttpFoundation\Request;
*/
function system_requirements($phase) {
global $install_state;
// Get the current default PHP requirements for this version of Drupal.
$minimum_supported_php = PhpRequirements::getMinimumSupportedPhp();
// Reset the extension lists.
/** @var \Drupal\Core\Extension\ModuleExtensionList $module_extension_list */
$module_extension_list = \Drupal::service('extension.list.module');
@ -284,10 +289,10 @@ function system_requirements($phase) {
}
// Check if the PHP version is below what Drupal supports.
if (version_compare($phpversion, \Drupal::MINIMUM_SUPPORTED_PHP) < 0) {
if (version_compare($phpversion, $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 PHP requirements</a> page for more information.',
[
'%version' => \Drupal::MINIMUM_SUPPORTED_PHP,
'%version' => $minimum_supported_php,
'%recommended' => \Drupal::RECOMMENDED_PHP,
':php_requirements' => 'https://www.drupal.org/docs/system-requirements/php-requirements',
]

View File

@ -2,6 +2,7 @@
namespace Drupal\Tests\system\Functional\System;
use Drupal\Core\Utility\PhpRequirements;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\RequirementsPageTrait;
@ -47,6 +48,7 @@ class PhpRequirementTest extends BrowserTestBase {
* Tests status report messages regarding the PHP version.
*/
public function testStatusPage() {
$minimum_php_version = PhpRequirements::getMinimumSupportedPhp();
// Go to Administration.
$this->drupalGet('admin/reports/status');
$this->assertSession()->statusCodeEquals(200);
@ -57,13 +59,13 @@ class PhpRequirementTest extends BrowserTestBase {
// Verify that an error is displayed about the PHP version if it is below
// the minimum supported PHP.
if (version_compare($phpversion, \Drupal::MINIMUM_SUPPORTED_PHP) < 0) {
if (version_compare($phpversion, $minimum_php_version) < 0) {
$this->assertErrorSummaries(['PHP']);
$this->assertSession()->pageTextContains('Your PHP installation is too old. Drupal requires at least PHP ' . \Drupal::MINIMUM_SUPPORTED_PHP);
$this->assertSession()->pageTextContains('Your PHP installation is too old. Drupal requires at least PHP ' . $minimum_php_version);
}
// Otherwise, there should be no error.
else {
$this->assertSession()->pageTextNotContains('Your PHP installation is too old. Drupal requires at least PHP ' . \Drupal::MINIMUM_SUPPORTED_PHP);
$this->assertSession()->pageTextNotContains('Your PHP installation is too old. Drupal requires at least PHP ' . $minimum_php_version);
$this->assertSession()->pageTextNotContains('Errors found');
}

View File

@ -7,6 +7,7 @@ use Drupal\Core\Language\Language;
use Drupal\Core\Session\UserSession;
use Drupal\Core\Site\Settings;
use Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware;
use Drupal\Core\Utility\PhpRequirements;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\RequirementsPageTrait;
use GuzzleHttp\HandlerStack;
@ -252,7 +253,7 @@ abstract class InstallerTestBase extends BrowserTestBase {
* @see system_requirements()
*/
protected function setUpRequirementsProblem() {
if (version_compare(phpversion(), \Drupal::MINIMUM_SUPPORTED_PHP) < 0) {
if (version_compare(phpversion(), PhpRequirements::getMinimumSupportedPhp()) < 0) {
$this->continueOnExpectedWarnings(['PHP']);
}
}

View File

@ -437,8 +437,8 @@ class DrupalTest extends UnitTestCase {
* Tests the PHP constants have consistent values.
*/
public function testPhpConstants() {
// RECOMMENDED_PHP and MINIMUM_SUPPORTED_PHP can be just MAJOR.MINOR so
// normalize them so that version_compare() can be used.
// RECOMMENDED_PHP can be just MAJOR.MINOR so normalize it to allow using
// version_compare().
$normalizer = function (string $version): string {
// The regex below is from \Composer\Semver\VersionParser::normalize().
preg_match('{^(\d{1,5})(\.\d++)?(\.\d++)?$}i', $version, $matches);
@ -447,10 +447,8 @@ class DrupalTest extends UnitTestCase {
. (!empty($matches[3]) ? $matches[3] : '.9999999');
};
$minimum_supported_php = $normalizer(\Drupal::MINIMUM_SUPPORTED_PHP);
$recommended_php = $normalizer(\Drupal::RECOMMENDED_PHP);
$this->assertTrue(version_compare($minimum_supported_php, \Drupal::MINIMUM_PHP, '>='), "\Drupal::MINIMUM_SUPPORTED_PHP should be greater or equal to \Drupal::MINIMUM_PHP");
$this->assertTrue(version_compare($recommended_php, \Drupal::MINIMUM_SUPPORTED_PHP, '>='), "\Drupal::RECOMMENDED_PHP should be greater or equal to \Drupal::MINIMUM_SUPPORTED_PHP");
$this->assertTrue(version_compare($recommended_php, \Drupal::MINIMUM_PHP, '>='), "\Drupal::RECOMMENDED_PHP should be greater or equal to \Drupal::MINIMUM_PHP");
// As this test depends on the $normalizer function it is tested.
$this->assertSame('10.9999999.9999999', $normalizer('10'));

View File

@ -0,0 +1,184 @@
<?php
namespace Drupal\Tests\Core\Utility;
use Drupal\Core\Utility\PhpRequirements;
use Drupal\Tests\UnitTestCase;
/**
* Tests the \Drupal\Core\Utility\PhpRequirements class.
*
* @coversDefaultClass \Drupal\Core\Utility\PhpRequirements
* @group Utility
*/
class PhpRequirementsTest extends UnitTestCase {
/**
* Ensures that PHP EOL dates are valid.
*
* This ensures that that all of the PHP EOL Date items are valid ISO 8601
* dates and are keyed by a valid version number.
*/
public function testPhpEolDates(): void {
$reflected = new \ReflectionClass(PhpRequirements::class);
$php_eol_dates = $reflected->getStaticPropertyValue('phpEolDates');
foreach ($php_eol_dates as $version => $eol_date) {
// Ensure that all of the version numbers are defined in a superset of
// semver: 'major.minor.patch-modifier', where (unlike in semver) all
// parts but the major are optional.
// @see version_compare()
$this->assertMatchesRegularExpression('/^([0-9]+)(\.([0-9]+)(\.([0-9]+)(-[A-Za-z0-9]+)?)?)?$/', $version);
// Ensure that all of the EOL dates are defined using ISO 8601 format.
$this->assertMatchesRegularExpression('/^([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])$/', $eol_date);
}
// Ensure that the EOL list is sorted in an ascending order by the date. If
// there are multiple versions EOL on the same day, sort by the PHP
// version.
uksort($php_eol_dates, function ($a, $b) use ($php_eol_dates) {
$a_date = strtotime($php_eol_dates[$a]);
$b_date = strtotime($php_eol_dates[$b]);
if ($a_date === $b_date) {
return $a <=> $b;
}
return $a_date <=> $b_date;
});
$this->assertSame($php_eol_dates, $reflected->getStaticPropertyValue('phpEolDates'));
}
/**
* Tests the minimum supported PHP for valid scenarios.
*
* @param string $date_string
* A valid PHP date string for the date to check.
* @param string $drupal_minimum_php
* The PHP minimum version hard requirement for the Drupal version, below
* which Drupal cannot be installed or updated, typically
* \Drupal::MINIMUM_PHP.
* @param string[] $php_eol_dates
* Associative array of PHP version EOL date strings, keyed by the PHP minor
* version.
* @param string $expected_php_version
* The PHP version the test should recommend.
*
* @covers ::getMinimumSupportedPhp
*
* @dataProvider providerMinimumSupportedPhp
*/
public function testMinimumSupportedPhp(string $date_string, string $drupal_minimum_php, array $php_eol_dates, string $expected_php_version): void {
$reflected = new \ReflectionClass(PhpRequirements::class);
$reflected->setStaticPropertyValue('drupalMinimumPhp', $drupal_minimum_php);
$reflected->setStaticPropertyValue('phpEolDates', $php_eol_dates);
$date = new \DateTime($date_string);
$this->assertSame($expected_php_version, PhpRequirements::getMinimumSupportedPhp($date));
}
/**
* Data provider for ::testMinimumSupportedPhp().
*
* See the parameter documentation of testMinimumSupportedPhp() for the test
* array structure. The last element is the expected minimum supported PHP.
*
* @return \Generator
* Test scenarios.
*/
public function providerMinimumSupportedPhp(): \Generator {
$eol_lists = [];
// Test against the known valid data from 9.0.0 to 9.3.0.
$eol_lists['d9_release'] = [
'7.2' => '2020-11-30',
'7.3' => '2021-12-06',
'7.4' => '2022-11-28',
'8.0' => '2023-11-26',
'8.1' => '2024-11-25',
];
// The actual situation the day of 9.0.0's release.
yield ['2020-06-03', '7.3.0', $eol_lists['d9_release'], '7.3.0'];
// If Drupal's MINIMUM_PHP had been 7.3.12 then.
yield ['2020-06-03', '7.3.12', $eol_lists['d9_release'], '7.3.12'];
// If Drupal's MINIMUM_PHP had been 7.2.17 then.
yield ['2020-06-03', '7.2.17', $eol_lists['d9_release'], '7.2.17'];
// If Drupal's MINIMUM_PHP had been 7.1.5 then.
yield ['2020-06-03', '7.1.5', $eol_lists['d9_release'], '7.2'];
// If the PHP EOL date list were empty.
yield ['2020-06-03', '7.3.0', [], '7.3.0'];
// Cases around PHP 7.2's EOL.
yield ['2020-11-29', '7.3.0', $eol_lists['d9_release'], '7.3.0'];
yield ['2020-11-30', '7.3.0', $eol_lists['d9_release'], '7.3.0'];
yield ['2020-12-01', '7.3.0', $eol_lists['d9_release'], '7.3.0'];
// Cases around PHP 7.3's EOL.
yield ['2021-12-05', '7.3.0', $eol_lists['d9_release'], '7.3.0'];
yield ['2021-12-06', '7.3.0', $eol_lists['d9_release'], '7.4'];
yield ['2021-12-07', '7.3.0', $eol_lists['d9_release'], '7.4'];
// Cases around PHP 7.4's EOL.
yield ['2022-11-27', '7.3.0', $eol_lists['d9_release'], '7.4'];
yield ['2022-11-28', '7.3.0', $eol_lists['d9_release'], '8.0'];
yield ['2022-11-29', '7.3.0', $eol_lists['d9_release'], '8.0'];
// Cases around PHP 8.0's EOL.
yield ['2023-11-25', '7.3.0', $eol_lists['d9_release'], '8.0'];
yield ['2023-11-26', '7.3.0', $eol_lists['d9_release'], '8.1'];
yield ['2023-11-27', '7.3.0', $eol_lists['d9_release'], '8.1'];
// Cases around PHP 8.1's EOL, without any data for 8.2.
yield ['2024-11-24', '7.3.0', $eol_lists['d9_release'], '8.1'];
yield ['2024-11-25', '7.3.0', $eol_lists['d9_release'], '8.1'];
yield ['2024-11-26', '7.3.0', $eol_lists['d9_release'], '8.1'];
// Cases for Drupal 10, with its current 8.0.2 MINIMUM_PHP, prior to PHP
// 8.0's EOL.
yield ['2021-12-05', '8.0.2', $eol_lists['d9_release'], '8.0.2'];
yield ['2021-12-06', '8.0.2', $eol_lists['d9_release'], '8.0.2'];
yield ['2021-12-07', '8.0.2', $eol_lists['d9_release'], '8.0.2'];
yield ['2022-11-27', '8.0.2', $eol_lists['d9_release'], '8.0.2'];
yield ['2022-11-28', '8.0.2', $eol_lists['d9_release'], '8.0.2'];
yield ['2022-11-29', '8.0.2', $eol_lists['d9_release'], '8.0.2'];
// Cases for Drupal 10 around PHP 8.0's EOL.
yield ['2023-11-25', '8.0.2', $eol_lists['d9_release'], '8.0.2'];
yield ['2023-11-26', '8.0.2', $eol_lists['d9_release'], '8.1'];
yield ['2023-11-27', '8.0.2', $eol_lists['d9_release'], '8.1'];
// Cases for Drupal 10 around and after PHP 8.1's EOL, without any data
// for 8.2.
yield ['2024-11-24', '8.0.2', $eol_lists['d9_release'], '8.1'];
yield ['2024-11-25', '8.0.2', $eol_lists['d9_release'], '8.1'];
yield ['2024-11-26', '8.0.2', $eol_lists['d9_release'], '8.1'];
yield ['2027-01-01', '8.0.2', $eol_lists['d9_release'], '8.1'];
// Test against a hypothetical set of PHP versions that have an LTS
// (supported longer than subsequent versions).
$eol_lists['php_with_lts'] = $eol_lists['d9_release'];
// Ensure that the PHP version with longest support is listed last.
unset($eol_lists['php_with_lts']['7.4']);
$eol_lists['php_with_lts']['7.4'] = '2025-11-28';
yield ['2021-12-05', '7.3', $eol_lists['php_with_lts'], '7.3'];
yield ['2021-12-06', '7.3', $eol_lists['php_with_lts'], '7.4'];
yield ['2022-11-28', '7.3', $eol_lists['php_with_lts'], '7.4'];
yield ['2023-11-26', '7.3', $eol_lists['php_with_lts'], '7.4'];
yield ['2024-11-25', '7.3', $eol_lists['php_with_lts'], '7.4'];
yield ['2025-12-01', '7.3', $eol_lists['php_with_lts'], '7.4'];
// Case with multiple versions EOL on the same day.
$eol_lists['same_eol_date'] = $eol_lists['d9_release'];
$eol_lists['same_eol_date']['8.2'] = $eol_lists['same_eol_date']['8.1'];
yield ['2021-12-05', '7.3', $eol_lists['same_eol_date'], '7.3'];
yield ['2023-11-27', '8.0.2', $eol_lists['same_eol_date'], '8.1'];
yield ['2027-07-31', '8.0.2', $eol_lists['same_eol_date'], '8.2'];
}
}

View File

@ -2,6 +2,8 @@
namespace Drupal\Tests;
use Drupal\Core\Utility\PhpRequirements;
/**
* Provides helper methods for the requirements page.
*/
@ -13,7 +15,9 @@ trait RequirementsPageTrait {
protected function updateRequirementsProblem() {
// Assert a warning is shown on older test environments.
$links = $this->getSession()->getPage()->findAll('named', ['link', 'try again']);
if ($links && version_compare(phpversion(), \Drupal::MINIMUM_SUPPORTED_PHP) < 0) {
// Get the default Drupal core PHP requirements.
if ($links && version_compare(phpversion(), PhpRequirements::getMinimumSupportedPhp()) < 0) {
$this->assertSession()->pageTextNotContains('Errors found');
$this->assertWarningSummaries(['PHP']);
$this->clickLink('try again');