From a416b4d9237f74b19cdd47855f3b46fef0990843 Mon Sep 17 00:00:00 2001 From: Mark Randall Date: Wed, 17 Jun 2026 22:16:09 +0100 Subject: [PATCH 1/6] Add more reliable way of accessing branch / version / release data without having to rely on global-scoped variables. --- bin/bumpRelease | 13 +- include/branch-overrides.inc | 33 ++ include/branches.inc | 125 +++-- include/gpg-keys.inc | 5 +- include/releases.inc | 2 +- include/version.inc | 75 +-- phpstan-baseline.neon | 92 ++-- phpstan.neon.dist | 6 + public/eol.php | 8 +- public/images/supported-versions.php | 18 +- public/index.php | 5 +- public/releases/active.php | 5 +- public/releases/branches.php | 17 +- public/releases/feed.php | 4 + public/releases/index.php | 12 +- public/releases/states.php | 13 +- public/supported-versions.php | 14 +- src/Releases/Branches.php | 434 ++++++++++++++++++ src/autoload.php | 2 + .../Releases/LegacyReleaseHelpersTest.php | 191 ++++++++ tests/phpunit.xml | 2 - 21 files changed, 869 insertions(+), 207 deletions(-) create mode 100644 include/branch-overrides.inc create mode 100644 src/Releases/Branches.php create mode 100644 tests/Unit/Releases/LegacyReleaseHelpersTest.php diff --git a/bin/bumpRelease b/bin/bumpRelease index f03c2c6da9..50ecf6bedc 100755 --- a/bin/bumpRelease +++ b/bin/bumpRelease @@ -1,21 +1,24 @@ #!/usr/bin/env php [ + 'security' => '2000-10-20', + ], + '5.3' => [ + 'stable' => '2013-07-11', + 'security' => '2014-08-14', + ], + '5.4' => [ + 'stable' => '2014-09-14', + 'security' => '2015-09-03', + ], + '5.5' => [ + 'stable' => '2015-07-10', + 'security' => '2016-07-21', + ], + '5.6' => [ + 'stable' => '2017-01-19', + 'security' => '2018-12-31', + ], + '7.0' => [ + 'stable' => '2018-01-04', + 'security' => '2019-01-10', + ], + '8.4' => [ + 'date' => '2024-11-21', + ], +]; diff --git a/include/branches.inc b/include/branches.inc index deb1f79841..2111319261 100644 --- a/include/branches.inc +++ b/include/branches.inc @@ -1,5 +1,7 @@ [ - 'security' => '2000-10-20', - ], - '5.3' => [ - 'stable' => '2013-07-11', - 'security' => '2014-08-14', - ], - '5.4' => [ - 'stable' => '2014-09-14', - 'security' => '2015-09-03', - ], - '5.5' => [ - 'stable' => '2015-07-10', - 'security' => '2016-07-21', - ], - '5.6' => [ - 'stable' => '2017-01-19', - 'security' => '2018-12-31', - ], - '7.0' => [ - 'stable' => '2018-01-04', - 'security' => '2019-01-10', - ], - '8.4' => [ - 'date' => '2024-11-21', - ], -]; +$BRANCHES = require __DIR__ . '/branch-overrides.inc'; /* Time to keep EOLed branches in the array returned by get_active_branches(), * which is used on the front page download links and the supported versions @@ -98,6 +70,7 @@ function version_number_to_branch(string $version): ?string { return null; } +#[Deprecated('Use Branches::get_all_branches()')] function get_all_branches() { $branches = []; @@ -135,7 +108,8 @@ function get_all_branches() { return $branches; } -function get_active_branches($include_recent_eols = true) { +#[Deprecated('Use Branches::active()')] +function get_active_branches(bool $include_recent_eols = true) { $branches = []; $now = new DateTime(); @@ -170,6 +144,7 @@ function get_active_branches($include_recent_eols = true) { /* If you provide an array to $always_include, note that the version numbers * must be in $RELEASES _and_ must be the full version number, not the branch: * ie provide array('5.3.29'), not array('5.3'). */ +#[Deprecated('Use Branches::eol()')] function get_eol_branches($always_include = null) { $always_include = $always_include ?: []; $branches = []; @@ -246,6 +221,7 @@ function get_eol_branches($always_include = null) { * MAJOR.MINOR.REVISION (the REVISION will be ignored if provided). This will * return either null (if no release exists on the given branch), or the usual * version metadata from $RELEASES for a single release. */ +#[Deprecated('Use Branches::getInitialRelease')] function get_initial_release($branch) { $branch = version_number_to_branch($branch); if (!$branch) { @@ -274,6 +250,7 @@ function get_initial_release($branch) { return null; } +#[Deprecated('Use Branches::getFinalRelease')] function get_final_release($branch) { $branch = version_number_to_branch($branch); if (!$branch) { @@ -305,6 +282,7 @@ function get_final_release($branch) { return null; } +#[Deprecated('Use Branches::getBranchBugsEOLDate')] function get_branch_bug_eol_date($branch): ?DateTime { if (isset($GLOBALS['BRANCHES'][$branch]['stable'])) { @@ -324,6 +302,7 @@ function get_branch_bug_eol_date($branch): ?DateTime return $date?->setDate($date->format('Y'), 12, 31); } +#[Deprecated('Use Branches::getBranchSecurityEOLDate')] function get_branch_security_eol_date($branch): ?DateTime { if (isset($GLOBALS['BRANCHES'][$branch]['security'])) { @@ -351,6 +330,7 @@ function get_branch_security_eol_date($branch): ?DateTime return $date?->setDate($date->format('Y'), 12, 31); } +#[Deprecated('Use Branches::getBranchReleaseDate')] function get_branch_release_date($branch): ?DateTime { $initial = get_initial_release($branch); @@ -358,6 +338,7 @@ function get_branch_release_date($branch): ?DateTime return isset($initial['date']) ? new DateTime($initial['date']) : null; } +#[Deprecated('Use Branches::getBranchSupportState')] function get_branch_support_state($branch) { $initial = get_branch_release_date($branch); $bug = get_branch_bug_eol_date($branch); @@ -399,7 +380,7 @@ function compare_version(array $arrayA, string $versionB) return 0; } -function version_array(string $version, ?int $length = null) +function version_array(string $version, ?int $length = null): mixed { $versionArray = array_map( 'intval', @@ -419,6 +400,7 @@ function version_array(string $version, ?int $length = null) return $versionArray; } +#[Deprecated('Use Branches::getCurrentReleaseForBranch')] function get_current_release_for_branch(int $major, ?int $minor): ?string { global $RELEASES, $OLDRELEASES; @@ -441,3 +423,80 @@ function get_current_release_for_branch(int $major, ?int $minor): ?string { return null; } + + +// Get latest release version and info. +/** + * @return array{mixed,mixed} + */ +function release_get_latest(): array { + $RELEASES = Branches::getReleaseData(); + + $version = '0.0.0'; + $current = null; + foreach ($RELEASES as $versions) { + foreach ($versions as $ver => $info) { + if (version_compare($ver, $version) > 0) { + $version = $ver; + $current = $info; + } + } + } + + return [$version, $current]; +} + +function show_source_releases(): void +{ + $RELEASES = Branches::getReleaseData(); + + $SHOW_COUNT = 4; + + $current_uri = htmlspecialchars($_SERVER['REQUEST_URI'], ENT_QUOTES, 'UTF-8'); + + $i = 0; foreach ($RELEASES as $MAJOR => $major_releases): /* major releases loop start */ + $releases = array_slice($major_releases, 0, $SHOW_COUNT); + ?> + + $a): ?> + + + +

+ + PHP + (Changelog) +

+
+ + + + GPG Keys for PHP +
+ + + array ( '8.4.21' => diff --git a/include/version.inc b/include/version.inc index 511f8e9536..aa210c0ac9 100644 --- a/include/version.inc +++ b/include/version.inc @@ -15,7 +15,7 @@ * ), * ); */ -$RELEASES = (function () { +return $RELEASES = (function () { $data = []; /* PHP 8.5 Release */ @@ -74,6 +74,7 @@ $RELEASES = (function () { [$major] = explode('.', $version, 2); $info = [ + 'version' => $version, 'announcement' => $release['announcement'] ?? true, 'tags' => $release['tags'], 'date' => $release['date'], @@ -91,75 +92,3 @@ $RELEASES = (function () { } return $ret; })(); - -// Get latest release version and info. -function release_get_latest() { - global $RELEASES; - - $version = '0.0.0'; - $current = null; - foreach ($RELEASES as $versions) { - foreach ($versions as $ver => $info) { - if (version_compare($ver, $version) > 0) { - $version = $ver; - $current = $info; - } - } - } - - return [$version, $current]; -} - -function show_source_releases() -{ - global $RELEASES; - - $SHOW_COUNT = 4; - - $current_uri = htmlspecialchars($_SERVER['REQUEST_URI'], ENT_QUOTES, 'UTF-8'); - - $i = 0; foreach ($RELEASES as $MAJOR => $major_releases): /* major releases loop start */ - $releases = array_slice($major_releases, 0, $SHOW_COUNT); -?> - - $a): ?> - - - -

- - PHP - (Changelog) -

-
- - - - GPG Keys for PHP -
- - -\|false given\.$#' identifier: argument.type count: 2 path: include/branches.inc + - + message: '#^Parameter \#3 \$length of function substr expects int\|null, int\|false given\.$#' + identifier: argument.type + count: 1 + path: include/branches.inc + + - + message: '#^Result of && is always false\.$#' + identifier: booleanAnd.alwaysFalse + count: 1 + path: include/branches.inc + - message: '#^Function bugfix\(\) has parameter \$number with no type specified\.$#' identifier: missingType.parameter @@ -1314,30 +1326,12 @@ parameters: count: 1 path: include/site.inc - - - message: '#^Function release_get_latest\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: include/version.inc - - - - message: '#^Function show_source_releases\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: include/version.inc - - message: '#^Offset ''announcement'' on array\{version\: ''8\.2\.31'', date\: ''07 May 2026'', tags\: array\{''security''\}, sha256\: array\{''tar\.gz''\: ''083c2f61cc5f527eb29…'', ''tar\.bz2''\: ''948183fa04cf261c9b9…'', ''tar\.xz''\: ''95eae411d594fe6f6e5…''\}\}\|array\{version\: ''8\.3\.31'', date\: ''07 May 2026'', tags\: array\{''security''\}, sha256\: array\{''tar\.gz''\: ''4e7baaf0a690e954a20…'', ''tar\.bz2''\: ''e6986b1fd37eb254021…'', ''tar\.xz''\: ''66410cee07f4b2baeb0…''\}\}\|array\{version\: ''8\.4\.22'', date\: ''04 Jun 2026'', tags\: array\{\}, sha256\: array\{''tar\.gz''\: ''a012c2c9724baf214a7…'', ''tar\.bz2''\: ''4b16e7e2c384ce25e07…'', ''tar\.xz''\: ''696c0f6ad92e94c5905…''\}\}\|array\{version\: ''8\.5\.7'', date\: ''04 Jun 2026'', tags\: array\{\}, sha256\: array\{''tar\.gz''\: ''e5eba93fd6dd3241d0e…'', ''tar\.bz2''\: ''4ef9355f784d4b32015…'', ''tar\.xz''\: ''01ba2ed1c2658dacf91…''\}\} on left side of \?\? does not exist\.$#' identifier: nullCoalesce.offset count: 1 path: include/version.inc - - - message: '#^Parameter \#3 \$length of function substr expects int\|null, int\|false given\.$#' - identifier: argument.type - count: 1 - path: include/version.inc - - message: '#^Variable \$SIDEBAR_DATA might not be defined\.$#' identifier: variable.undefined @@ -2323,10 +2317,16 @@ parameters: path: public/releases/feed.php - - message: '#^Variable \$RELEASES might not be defined\.$#' - identifier: variable.undefined + message: '#^Cannot access offset ''date'' on array\{version\: string, announcement\: bool, tags\: list\, date\: string, source\: list\\}\|false\.$#' + identifier: offsetAccess.nonOffsetAccessible count: 1 - path: public/releases/feed.php + path: public/releases/index.php + + - + message: '#^Cannot access offset ''supported_versions'' on array\{version\: string, announcement\: bool, tags\: list\, date\: string, source\: list\\}\|false\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: public/releases/index.php - message: '#^Function mk_rel\(\) has parameter \$announcement with no value type specified in iterable type array\.$#' @@ -2347,21 +2347,27 @@ parameters: path: public/releases/index.php - - message: '#^Parameter \#1 \.\.\.\$arg1 of function max expects non\-empty\-array, list\ given\.$#' - identifier: argument.type + message: '#^Offset ''announcement'' on array\{version\: string, announcement\: bool, tags\: list\, date\: string, source\: list\\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset count: 1 path: public/releases/index.php - - message: '#^Variable \$OLDRELEASES might not be defined\.$#' - identifier: variable.undefined - count: 3 + message: '#^Offset ''museum'' on array\{version\: string, announcement\: bool, tags\: list\, date\: string, source\: list\\} on left side of \?\? does not exist\.$#' + identifier: nullCoalesce.offset + count: 1 path: public/releases/index.php - - message: '#^Variable \$RELEASES might not be defined\.$#' - identifier: variable.undefined - count: 2 + message: '#^Offset ''source'' on array\{version\: string, announcement\: bool, tags\: list\, date\: string, source\: list\\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: public/releases/index.php + + - + message: '#^Offset ''windows'' on array\{version\: string, announcement\: bool, tags\: list\, date\: string, source\: list\\} on left side of \?\? does not exist\.$#' + identifier: nullCoalesce.offset + count: 1 path: public/releases/index.php - @@ -2430,18 +2436,6 @@ parameters: count: 1 path: public/submit-event.php - - - message: '#^Parameter \#1 \$string of function htmlspecialchars expects string, float\|string given\.$#' - identifier: argument.type - count: 1 - path: public/supported-versions.php - - - - message: '#^Possibly invalid array key type float\|string\.$#' - identifier: offsetAccess.invalidOffset - count: 1 - path: public/supported-versions.php - - message: '#^Variable \$COUNTRIES might not be defined\.$#' identifier: variable.undefined diff --git a/phpstan.neon.dist b/phpstan.neon.dist index ee0bff472b..8f6c30d214 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -19,3 +19,9 @@ parameters: - include/releases.inc - include/pregen-news.inc - include/pregen-confs.inc + + # Can do cleanup after, without the previous items themsleves being new errors + reportUnmatchedIgnoredErrors: false + + editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%' + editorUrlTitle: '%%relFile%%:%%line%%' diff --git a/public/eol.php b/public/eol.php index 9a675c43aa..ec8f37207b 100644 --- a/public/eol.php +++ b/public/eol.php @@ -1,8 +1,10 @@ - $branches): ?> + $branches): ?> $detail): ?> - + diff --git a/public/images/supported-versions.php b/public/images/supported-versions.php index 80434bbb8c..f2f695f3a5 100644 --- a/public/images/supported-versions.php +++ b/public/images/supported-versions.php @@ -1,6 +1,8 @@ $version) { - if (version_compare($branch, '5.3', 'ge') && get_branch_security_eol_date($branch) > min_date()) { + if (version_compare($branch, '5.3', 'ge') && Branches::getBranchSecurityEOLDate($branch) > min_date()) { $branches[$branch] = $version; } } @@ -121,7 +123,7 @@ function date_horiz_coord(DateTime $date) { $version): ?> - + @@ -134,9 +136,9 @@ function date_horiz_coord(DateTime $date) { $version): ?> diff --git a/public/index.php b/public/index.php index 6758a83781..00b8597a17 100644 --- a/public/index.php +++ b/public/index.php @@ -1,6 +1,7 @@ \n"; -$active_branches = get_active_branches(); +$active_branches = Branches::active(); krsort($active_branches); foreach ($active_branches as $major => $releases) { krsort($releases); diff --git a/public/releases/active.php b/public/releases/active.php index d2cfca2862..1e3e77b59c 100644 --- a/public/releases/active.php +++ b/public/releases/active.php @@ -1,12 +1,11 @@ $releases) { +foreach (Branches::all() as $major => $releases) { foreach ($releases as $branch => $release) { $current[$branch] = [ 'branch' => $branch, - 'latest' => ($release['version'] ?? null), - 'state' => get_branch_support_state($branch), - 'initial_release' => formatDate(get_branch_release_date($branch)), - 'active_support_end' => formatDate(get_branch_bug_eol_date($branch)), - 'security_support_end' => formatDate(get_branch_security_eol_date($branch)), + 'latest' => $release['version'], + 'state' => Branches::getBranchSupportStatus($branch), + 'initial_release' => formatDate(Branches::getBranchReleaseDate($branch)), + 'active_support_end' => formatDate(Branches::getBranchBugsEOLDate($branch)), + 'security_support_end' => formatDate(Branches::getBranchSecurityEOLDate($branch)), ]; } } diff --git a/public/releases/feed.php b/public/releases/feed.php index bc5eaf97de..2a1b391250 100644 --- a/public/releases/feed.php +++ b/public/releases/feed.php @@ -1,9 +1,13 @@ diff --git a/public/releases/index.php b/public/releases/index.php index cd4d0073de..794c9a2e6b 100644 --- a/public/releases/index.php +++ b/public/releases/index.php @@ -1,8 +1,12 @@ $releases) { + foreach (Branches::active() as $major => $releases) { $supportedVersions[$major] = array_keys($releases); } @@ -53,9 +57,7 @@ } } else { foreach ($RELEASES as $major => $release) { - $version = key($release); $r = current($release); - $r["version"] = $version; $r['supported_versions'] = $supportedVersions[$major] ?? []; $machineReadable[$major] = $r; } @@ -84,6 +86,8 @@

\n"; $active_majors = array_keys($RELEASES); +assert(!empty($active_majors)); + $latest = max($active_majors); foreach ($OLDRELEASES as $major => $a) { echo ''; diff --git a/public/releases/states.php b/public/releases/states.php index 429214563c..97a8d9660f 100644 --- a/public/releases/states.php +++ b/public/releases/states.php @@ -3,12 +3,11 @@ # Please use /releases/branches.php instead. # This API *may* be removed at an indeterminate point in the future. -use phpweb\ProjectGlobals; +use phpweb\Releases\Branches; $_SERVER['BASE_PAGE'] = 'releases/active.php'; require_once __DIR__ . '/../../include/prepend.inc'; -require_once ProjectGlobals::getPublicRoot() . '/include/branches.inc'; header('Content-Type: application/json; charset=UTF-8'); @@ -18,14 +17,14 @@ function formatDate($date = null) { return $date !== null ? $date->format('c') : null; } -foreach (get_all_branches() as $major => $releases) { +foreach (Branches::all() as $major => $releases) { $states[$major] = []; foreach ($releases as $branch => $release) { $states[$major][$branch] = [ - 'state' => get_branch_support_state($branch), - 'initial_release' => formatDate(get_branch_release_date($branch)), - 'active_support_end' => formatDate(get_branch_bug_eol_date($branch)), - 'security_support_end' => formatDate(get_branch_security_eol_date($branch)), + 'state' => Branches::getBranchSupportStatus($branch), + 'initial_release' => formatDate(Branches::getBranchReleaseDate($branch)), + 'active_support_end' => formatDate(Branches::getBranchBugsEOLDate($branch)), + 'security_support_end' => formatDate(Branches::getBranchSecurityEOLDate($branch)), ]; } krsort($states[$major]); diff --git a/public/supported-versions.php b/public/supported-versions.php index c7d98b6097..78571eab77 100644 --- a/public/supported-versions.php +++ b/public/supported-versions.php @@ -1,8 +1,10 @@ ['supported-versions.css']]); @@ -51,14 +53,14 @@ - $releases): ?> + $releases): ?> $release): ?> diff --git a/src/Releases/Branches.php b/src/Releases/Branches.php new file mode 100644 index 0000000000..fe80b6667e --- /dev/null +++ b/src/Releases/Branches.php @@ -0,0 +1,434 @@ +, + * date: string, + * source: list + * } + */ +class Branches +{ + /** + * @return array> + */ + public static function getReleaseData(): array + { + static $cache = null; + + /* there is no normalisation required here because it's all standard format */ + return $cache ??= require __DIR__ . '/../../include/version.inc'; + } + + /** + * @return array> + */ + public static function getOldReleaseData(): array + { + static $cache = null; + + return $cache ??= (function () { + $original = require __DIR__ . '/../../include/releases.inc'; + + foreach ($original as &$releases) { + foreach ($releases as $releaseId => &$release) { + /* always force the version to be copied into the array, some normalized steps do it anyway */ + $release['version'] = $releaseId; + + /* only care for true or false here */ + $announcement = $release['announcement'] ?? null; + if (is_array($announcement)) { + $release['announcement'] = !empty($announcement); + } + + /* we have release announcements going back to 4.1.0 in releases/x_y_z.php */ + $release['announcement'] ??= version_compare($release['version'], '4.1.0', '>='); + + /* if any of the source files do not have a `filename` they are invalid */ + foreach ($release['source'] as $sIdx => $source) { + if (!isset($source['filename'])) { + unset($release['source'][$sIdx]); + } + } + } + } + + return $original; + })(); + } + + /** + * @return array + */ + public static function getBranchOverrides(): array + { + static $cache = null; + return $cache ??= require __DIR__ . '/../../include/branch-overrides.inc'; + } + + /** + * @return array> + */ + public static function all(): array + { + static $cache = null; + return $cache ??= (function () { + $results = []; + foreach (self::getReleaseData() as $majorVersion => $releases) { + foreach ($releases as $releaseId => $release) { + $results[$majorVersion][$releaseId] = $release; + } + } + + foreach (self::getOldReleaseData() as $majorVersion => $releases) { + foreach ($releases as $releaseId => $release) { + $results[$majorVersion][$releaseId] = $release; + } + } + + return $results; + })(); + } + + /** + * Returns an associative array [$major][$major.$minor] = $release where $release is the + * standard data struct. In effect, finding the last version for each. + * + * Previously get_all_branches + * + * @return array> + */ + public static function get_all_branches(): array + { + $GLOBAL_OLDRELEASES = self::getOldReleaseData(); + $GLOBAL_RELEASES = self::getReleaseData(); + $branches = []; + + foreach ($GLOBAL_OLDRELEASES as $major => $releases) { + foreach ($releases as $version => $release) { + $branch = self::versionToBranch($version); + + if (!isset($branches[$major][$branch]) + || version_compare($version, $branches[$major][$branch]['version'], 'gt') + ) { + $branches[$major][$branch] = $release; + } + } + } + + foreach ($GLOBAL_RELEASES as $major => $releases) { + foreach ($releases as $version => $release) { + $branch = self::versionToBranch($version); + + if (!isset($branches[$major][$branch]) + || version_compare($version, $branches[$major][$branch]['version'], 'gt') + ) { + $branches[$major][$branch] = $release; + } + } + } + + krsort($branches); + foreach ($branches as &$branch) { + krsort($branch); + } + + return $branches; + } + + /** + * @param bool $include_recent_eols + * @return array> + */ + public static function active(bool $include_recent_eols = true): array + { + $recentInterval = new DateInterval('P28D'); + + $GLOBAL_RELEASES = self::getReleaseData(); + $branches = []; + $now = new DateTime(); + + foreach ($GLOBAL_RELEASES as $major => $releases) { + foreach ($releases as $releaseId => $release) { + $branch = self::versionToBranch($releaseId); + + $threshold = self::getBranchSecurityEOLDate($branch); + if ($threshold === null) { + // No EOL date available, assume it is ancient. + continue; + } + + if ($include_recent_eols) { + $threshold->add($recentInterval); + } + + if ($now < $threshold) { + $branches[$major][$branch] = $release; + } + } + + if (!empty($branches[$major])) { + ksort($branches[$major]); + } + } + + ksort($branches); + + return $branches; + } + + /** + * @return array> + */ + public static function eol(): array + { + $GLOBAL_OLDRELEASES = self::getOldReleaseData(); + $GLOBAL_RELEASES = self::getReleaseData(); + + $branches = []; + $now = new DateTime(); + + // Gather the last release on each branch into a convenient array. + foreach ($GLOBAL_OLDRELEASES as $major => $releases) { + foreach ($releases as $version => $release) { + $branch = self::versionToBranch($version); + + if (!isset($branches[$major][$branch]) + || version_compare($version, $branches[$major][$branch]['version'], 'gt') + ) { + $branches[(string)$major][$branch] = [ + 'date' => $release['date'], + 'link' => "/releases#$version", + 'version' => $version, + ]; + } + } + } + + /* Exclude releases from active branches, where active is defined as "in + * the $RELEASES array and not explicitly marked as EOL there". */ + foreach ($GLOBAL_RELEASES as $major => $releases) { + foreach ($releases as $version => $release) { + $branch = self::versionToBranch($version); + + if ($now < self::getBranchSecurityEOLDate($branch)) { + /* This branch isn't EOL: remove it from our array. */ + if (isset($branches[$major][$branch])) { + unset($branches[$major][$branch]); + } + } + } + } + + krsort($branches); + foreach ($branches as &$branch) { + krsort($branch); + } + + return $branches; + } + + /** + * Finds the first release for a given branch + * + * @return NormalizedReleaseStruct|null + */ + public static function getInitialReleaseForBranch(string $branch): ?array + { + $all = self::all(); + $branch = self::versionToBranch($branch); + [$major] = explode('.', $branch); + + /* it seems that 8.4.0 is completely missing from the data */ + for ($patch = 0; $patch < 5; $patch++) { + $release = $all[$major][$branch . '.' . $patch] ?? null; + if ($release) { + return $release; + } + } + + return null; + } + + /** + * Finds the last release from a given branch + * + * @return NormalizedReleaseStruct|null + */ + public static function getFinalReleaseForBranch(string $branch): ?array + { + $GLOBAL_OLDRELEASES = self::getOldReleaseData(); + $GLOBAL_RELEASES = self::getReleaseData(); + $branch = self::versionToBranch($branch); + [$major] = explode('.', $branch); + + $last = "$branch.0"; + foreach ($GLOBAL_OLDRELEASES[$major] as $version => $release) { + if (self::versionToBranch($version) == $branch && version_compare($version, $last, '>')) { + $last = $version; + } + } + + if (isset($GLOBAL_OLDRELEASES[$major][$last])) { + return $GLOBAL_OLDRELEASES[$major][$last]; + } + + /* If there's only been one release on the branch, it won't be in + * $OLDRELEASES yet, so let's check $RELEASES. */ + if (isset($GLOBAL_RELEASES[$major][$last])) { + // Fake a date like we have on the oldreleases array. + $release = $GLOBAL_RELEASES[$major][$last]; + $release['date'] = $release['source'][0]['date']; + + return $release; + } + + // Shrug. + return null; + } + + public static function getBranchBugsEOLDate(string $branch): ?DateTime + { + $GLOBAL_BRANCHES = self::getBranchOverrides(); + + if (isset($GLOBAL_BRANCHES[$branch]['stable'])) { + return new DateTime($GLOBAL_BRANCHES[$branch]['stable']); + } + + $date = self::getBranchReleaseDate($branch); + + $date = $date?->add(new DateInterval('P2Y')); + + // Versions before 8.2 do not extend the release cycle to the end of the year + if (version_compare($branch, '8.2', '<')) { + return $date; + } + + // Extend the release cycle to the end of the year + return $date?->setDate((int)$date->format('Y'), 12, 31); + } + + public static function getBranchSecurityEOLDate(string $branch): ?DateTime + { + $GLOBAL_BRANCHES = self::getBranchOverrides(); + if (isset($GLOBAL_BRANCHES[$branch]['security'])) { + return new DateTime($GLOBAL_BRANCHES[$branch]['security']); + } + + /* Versions before 5.3 are based solely on the final release date in + * $OLDRELEASES. */ + if (version_compare($branch, '5.3', '<')) { + $release = self::getFinalReleaseForBranch($branch); + + return $release ? new DateTime($release['date']) : null; + } + + $date = self::getBranchReleaseDate($branch); + + // Versions before 8.1 have 3-year support since the initial release + if (version_compare($branch, '8.1', '<')) { + return $date?->add(new DateInterval('P3Y')); + } + + $date = $date?->add(new DateInterval('P4Y')); + + // Extend the release cycle to the end of the year + return $date?->setDate((int)$date->format('Y'), 12, 31); + } + + public static function getBranchReleaseDate(string $branch): ?DateTime + { + $initial = self::getInitialReleaseForBranch($branch); + + return isset($initial['date']) ? new DateTime($initial['date']) : null; + } + + public static function getBranchSupportStatus(string $branch): ?string + { + $initial = self::getBranchReleaseDate($branch); + $bug = self::getBranchBugsEOLDate($branch); + $security = self::getBranchSecurityEOLDate($branch); + + if ($initial && $bug && $security) { + $now = new DateTime(); + + if ($now >= $security) { + return 'eol'; + } + + if ($now >= $bug) { + return 'security'; + } + + if ($now >= $initial) { + return 'stable'; + } + + return 'future'; + } + + return null; + } + + public static function getCurrentReleaseForBranch(int $major, ?int $minor): ?string + { + $GLOBAL_RELEASES = self::getReleaseData(); + $GLOBAL_OLDRELEASES = self::getOldReleaseData(); + + $prefix = "{$major}."; + if ($minor !== null) { + $prefix .= "{$minor}."; + } + + foreach (($GLOBAL_RELEASES[$major] ?? []) as $version => $_) { + if (!strncmp($prefix, $version, strlen($prefix))) { + return $version; + } + } + + foreach (($GLOBAL_OLDRELEASES[$major] ?? []) as $version => $_) { + if (!strncmp($prefix, $version, strlen($prefix))) { + return $version; + } + } + + return null; + } + + private static function versionToBranch(string $version): string + { + $parts = explode('.', $version); + if (count($parts) > 1) { + return "$parts[0].$parts[1]"; + } + + throw new ValueError("Unexpected version '$version'"); + } +} diff --git a/src/autoload.php b/src/autoload.php index e69570a6b4..76c19ec9d5 100644 --- a/src/autoload.php +++ b/src/autoload.php @@ -1,5 +1,7 @@ $releases) { + foreach (array_keys($releases) as $releaseId) { + self::assertFalse(isset($olderReleases[$majorId][$releaseId]), "Duplicate data for '$releaseId'"); + } + } + } + + public function testAllReleasesContainsRecent(): void + { + $all = Branches::all(); + + foreach (Branches::getReleaseData() as $majorId => $releases) { + foreach (array_keys($releases) as $releaseId) { + self::assertTrue( + isset($all[$majorId][$releaseId]), + "Missing recent release data for '$releaseId' in all()", + ); + } + } + } + + public function testAllReleasesContainsOld(): void + { + $all = Branches::all(); + + foreach (Branches::getOldReleaseData() as $majorId => $releases) { + foreach (array_keys($releases) as $releaseId) { + self::assertTrue( + isset($all[$majorId][$releaseId]), + "Missing old release data for '$releaseId' in all()", + ); + } + } + } + + public function testNormalizationForAllData(): void + { + foreach (Branches::all() as $releases) { + foreach ($releases as $releaseId => $release) { + /* @phpstan-ignore-next-line */ + self::assertTrue( + /* @phpstan-ignore-next-line */ + isset($release['version']), + "Release '$releaseId' does not have a version defined", + ); + + /* @phpstan-ignore-next-line */ + self::assertTrue( + /* @phpstan-ignore-next-line */ + isset($release['date']), + "Release '$releaseId' does not have a date defined", + ); + + /* @phpstan-ignore-next-line */ + self::assertTrue( + /* @phpstan-ignore-next-line */ + isset($release['announcement']) && is_bool($release['announcement']), + "Release '$releaseId' does not have an announcement defined", + ); + + /* @phpstan-ignore-next-line */ + self::assertTrue( + /* @phpstan-ignore-next-line */ + isset($release['source']) && is_array($release['source']), + "Release '$releaseId' does not have a source list defined", + ); + + foreach ($release['source'] as $idx => $source) { + /* @phpstan-ignore-next-line */ + self::assertTrue( + /* @phpstan-ignore-next-line */ + isset($source['filename']), + "Release '$releaseId' source $idx does not have a filename", + ); + + /* @phpstan-ignore-next-line */ + self::assertTrue( + /* @phpstan-ignore-next-line */ + isset($source['name']), + "Release '$releaseId' source $idx does not have a name", + ); + } + } + } + } + + public function testActive(): void + { + self::assertNotEmpty(Branches::active()); + } + + public function testInitialReleases(): void + { + self::assertNotEmpty( + Branches::getInitialReleaseForBranch('8.5'), + 'Unable to find initial branch for 8.5', + ); + + self::assertNotEmpty( + Branches::getInitialReleaseForBranch('8.4'), + 'Unable to find initial branch for 8.4 (aborted release)', + ); + } + + /** + * These were dumped from the old functions and may break if additional + * information is added to the overrides at some point + */ + public static function provideExpectedDates(): Generator + { + yield '8.5' => ['8.5', '2025-11-20', '2027-12-31', '2029-12-31']; + yield '8.4' => ['8.4', '2024-11-21', '2026-12-31', '2028-12-31']; + yield '8.3' => ['8.3', '2023-11-23', '2025-12-31', '2027-12-31']; + yield '8.2' => ['8.2', '2022-12-08', '2024-12-31', '2026-12-31']; + yield '8.1' => ['8.1', '2021-11-25', '2023-11-25', '2025-12-31']; + yield '8.0' => ['8.0', '2020-11-26', '2022-11-26', '2023-11-26']; + yield '7.4' => ['7.4', '2019-11-28', '2021-11-28', '2022-11-28']; + yield '7.3' => ['7.3', '2018-12-06', '2020-12-06', '2021-12-06']; + yield '7.2' => ['7.2', '2017-11-30', '2019-11-30', '2020-11-30']; + yield '7.1' => ['7.1', '2016-12-01', '2018-12-01', '2019-12-01']; + yield '7.0' => ['7.0', '2015-12-03', '2018-01-04', '2019-01-10']; + yield '5.6' => ['5.6', '2014-08-28', '2017-01-19', '2018-12-31']; + yield '5.5' => ['5.5', '2013-06-20', '2015-07-10', '2016-07-21']; + yield '5.4' => ['5.4', '2012-03-01', '2014-09-14', '2015-09-03']; + yield '5.3' => ['5.3', '2009-06-30', '2013-07-11', '2014-08-14']; + yield '5.2' => ['5.2', '2006-11-02', '2008-11-02', '2011-01-06']; + yield '5.1' => ['5.1', '2005-11-24', '2007-11-24', '2006-08-24']; + yield '5.0' => ['5.0', '2004-07-13', '2006-07-13', '2005-09-05']; + yield '4.4' => ['4.4', '2005-07-11', '2007-07-11', '2008-08-07']; + yield '4.3' => ['4.3', '2002-12-27', '2004-12-27', '2005-03-31']; + yield '4.2' => ['4.2', '2002-04-22', '2004-04-22', '2002-09-06']; + yield '4.1' => ['4.1', '2001-12-10', '2003-12-10', '2002-03-12']; + yield '4.0' => ['4.0', '2000-05-22', '2002-05-22', '2001-06-23']; + + // 3.0 is not included as it's the only one which returns null + } + + #[DataProvider('provideExpectedDates')] + public function testExpectedDates(string $branch, string $initialDate, string $bugfixDate, string $securityDate): void + { + self::assertEquals( + $initialDate, + Branches::getBranchReleaseDate($branch)?->format('Y-m-d'), + ); + + self::assertEquals( + $bugfixDate, + Branches::getBranchBugsEOLDate($branch)?->format('Y-m-d'), + ); + + self::assertEquals( + $securityDate, + Branches::getBranchSecurityEOLDate($branch)?->format('Y-m-d'), + ); + } + + public function testCurrentReleaseForBranch(): void + { + /* need something that won't change in response to new releases */ + self::assertEquals( + '7.4.33', + Branches::getCurrentReleaseForBranch(7, 4), + ); + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 2e1031e36b..43f088f434 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -3,7 +3,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../vendor/phpunit/phpunit/phpunit.xsd" beStrictAboutChangesToGlobalState="true" - beStrictAboutCoverageMetadata="true" beStrictAboutOutputDuringTests="true" beStrictAboutTestsThatDoNotTestAnything="true" bootstrap="../src/autoload.php" @@ -18,7 +17,6 @@ displayDetailsOnTestsThatTriggerNotices="true" displayDetailsOnTestsThatTriggerWarnings="true" executionOrder="random" - requireCoverageMetadata="true" stopOnError="false" stopOnFailure="false" stopOnIncomplete="false" From 443d413cc743bf88dff324730ccef45306c9b572 Mon Sep 17 00:00:00 2001 From: Mark Randall Date: Thu, 18 Jun 2026 12:21:42 +0100 Subject: [PATCH 2/6] Update NewsHandler with docblock type, reusable filtering. Add reliable fallback for 'updated' => 'published'. --- include/layout.inc | 4 +- phpstan-baseline.neon | 18 ------- public/index.php | 2 +- src/News/NewsHandler.php | 109 ++++++++++++++++++++++++++++----------- 4 files changed, 84 insertions(+), 49 deletions(-) diff --git a/include/layout.inc b/include/layout.inc index f065310020..857128d64c 100644 --- a/include/layout.inc +++ b/include/layout.inc @@ -542,7 +542,7 @@ function get_news_changes() return false; } - $date = date_create($lastNews["updated"]); + $date = date_create($lastNews["updated"] ?? $lastNews["published"]); if (isset($_COOKIE["LAST_NEWS"]) && $_COOKIE["LAST_NEWS"] >= $date->getTimestamp()) { return false; } @@ -557,6 +557,8 @@ function get_news_changes() $date->modify("+1 week"); if ($date->getTimestamp() > $_SERVER["REQUEST_TIME"]) { + assert(isset($lastNews["link"][0]["href"])); + $link = preg_replace('~^(http://php.net/|https://www.php.net/)~', '/', $lastNews["link"][0]["href"]); $title = $lastNews["title"]; return "{$title}"; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index eba441413e..ed6401d99c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2532,24 +2532,6 @@ parameters: count: 1 path: src/News/Entry.php - - - message: '#^Call to function is_array\(\) with null will always evaluate to false\.$#' - identifier: function.impossibleType - count: 1 - path: src/News/NewsHandler.php - - - - message: '#^Method phpweb\\News\\NewsHandler\:\:getLastestNews\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/News/NewsHandler.php - - - - message: '#^Method phpweb\\News\\NewsHandler\:\:getPregeneratedNews\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/News/NewsHandler.php - - message: '#^Method phpweb\\Themes\\FeatureComparison\:\:__construct\(\) has parameter \$links with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue diff --git a/public/index.php b/public/index.php index 6758a83781..1694e3a246 100644 --- a/public/index.php +++ b/public/index.php @@ -61,7 +61,7 @@ foreach ((new NewsHandler())->getFrontPageNews() as $entry) { $link = preg_replace('~^(http://php.net/|https://www.php.net/)~', '', $entry["id"]); $id = parse_url($entry["id"], PHP_URL_FRAGMENT); - $date = date_create($entry['updated']); + $date = date_create($entry['updated'] ?? $entry['published']); $date_human = date_format($date, 'd M Y'); $date_w3c = date_format($date, DATE_W3C); $content .= <<, + * category: list, + * newsImage?: array{ + * link: string, + * content: string, + * }, + * content: string, + * intro?: string, + * finalTeaserDate?: string, + * } + */ final class NewsHandler { private const MAX_FRONT_PAGE_NEWS = 25; + /** + * @return NewsEntryStruct|null + */ public function getLastestNews(): array|null { $news = $this->getPregeneratedNews(); @@ -24,45 +51,48 @@ public function getLastestNews(): array|null return $news[0]; } - /** @return list */ - public function getFrontPageNews(): array + /** + * @param list $tags + * @return list + */ + public function getTaggedEntries(array $tags, ?int $limit = null): array { - $frontPage = []; + $entries = []; foreach ($this->getPregeneratedNews() as $entry) { - foreach ($entry['category'] as $category) { - if ($category['term'] !== 'frontpage') { - continue; - } - - $frontPage[] = $entry; - if (count($frontPage) >= self::MAX_FRONT_PAGE_NEWS) { - break 2; - } + if (!self::isTagged($entry, $tags)) { + continue; + } + + $entries[] = $entry; + if ($limit !== null && count($entries) >= $limit) { + break; } } - return $frontPage; + return $entries; } - /** @return list */ - public function getConferences(): array + /** + * Looks up generated news with frontpage tags. + * + * @return list + */ + public function getFrontPageNews(int $limit = self::MAX_FRONT_PAGE_NEWS): array { - $conferences = []; - foreach ($this->getPregeneratedNews() as $entry) { - foreach ($entry['category'] as $category) { - if ($category['term'] !== 'cfp' && $category['term'] !== 'conferences') { - continue; - } - - $conferences[] = $entry; - break; - } - } + return $this->getTaggedEntries(['frontpage'], $limit); + } - return $conferences; + /** + * @return list + */ + public function getConferences(?int $limit = null): array + { + return $this->getTaggedEntries(['cfp', 'conferences'], $limit); } - /** @return list */ + /** + * @return list + */ public function getNewsByYear(int $year): array { return array_values(array_filter( @@ -71,11 +101,32 @@ public function getNewsByYear(int $year): array )); } + /** + * @return list + */ public function getPregeneratedNews(): array { $NEWS_ENTRIES = null; include __DIR__ . '/../../include/pregen-news.inc'; + /** @phpstan-ignore-next-line - pregen-news sets global variable */ return is_array($NEWS_ENTRIES) ? $NEWS_ENTRIES : []; } + + /** + * @param NewsEntryStruct $data + * @param list|string $tags + */ + public static function isTagged(array $data, array|string $tags): bool + { + $tags = is_array($tags) ? $tags : [$tags]; + + foreach ($data['category'] as $category) { + if (in_array($category['term'], $tags, true)) { + return true; + } + } + + return false; + } } From 3c4e2be016bea8bc770229be4ac74d2718f71db7 Mon Sep 17 00:00:00 2001 From: Mark Randall Date: Sat, 20 Jun 2026 16:43:42 +0100 Subject: [PATCH 3/6] Most of the front page --- include/branch-highlights.inc | 495 +++++++++ include/footer.inc | 14 +- include/header.inc | 4 +- public/images/bg-texture-dark.png | 0 public/images/bg-texture-light.png | 0 public/images/community/libera.svg | 8 + public/images/community/linkedin.svg | 1 + public/images/community/mailing-lists.png | Bin 0 -> 12005 bytes public/images/community/mastodon.svg | 4 + public/images/community/phpc-discord.png | Bin 0 -> 36820 bytes .../community/phpdevelopers-discord.webp | Bin 0 -> 3792 bytes public/images/community/reddit.png | Bin 0 -> 80185 bytes public/images/icons/vendors/debian.svg | 0 .../icons/vendors/docker-ocean-blue.svg | 33 + public/images/icons/vendors/remi-repo.png | 0 .../language-development/documentation.png | Bin 0 -> 731748 bytes .../language-development/get-involved.png | Bin 0 -> 336415 bytes .../github_invertocat_white.svg | 10 + .../language-development/php-internals.png | Bin 0 -> 12005 bytes public/images/language-development/rfcs.png | Bin 0 -> 27795 bytes .../submit-bug-report.png | Bin 0 -> 12113 bytes public/images/logos/composer.png | 0 .../images/logos/github_invertocat_white.svg | 0 public/images/logos/php-foundation.svg | 0 public/images/vendors/composer.png | Bin 0 -> 104378 bytes public/images/vendors/php-foundation.svg | 11 + public/index.php | 952 ++++++++++++++---- public/styles/theme-gst.css | 688 +++++++++++++ public/styles/theme-medium.css | 13 +- src/Navigation/NavItem.php | 4 + src/Releases/VersionLogos.php | 139 +++ src/Themes/FooterRenderer.php | 141 +++ 32 files changed, 2321 insertions(+), 196 deletions(-) create mode 100644 include/branch-highlights.inc create mode 100644 public/images/bg-texture-dark.png create mode 100644 public/images/bg-texture-light.png create mode 100644 public/images/community/libera.svg create mode 100644 public/images/community/linkedin.svg create mode 100644 public/images/community/mailing-lists.png create mode 100644 public/images/community/mastodon.svg create mode 100644 public/images/community/phpc-discord.png create mode 100644 public/images/community/phpdevelopers-discord.webp create mode 100644 public/images/community/reddit.png create mode 100644 public/images/icons/vendors/debian.svg create mode 100644 public/images/icons/vendors/docker-ocean-blue.svg create mode 100644 public/images/icons/vendors/remi-repo.png create mode 100644 public/images/language-development/documentation.png create mode 100644 public/images/language-development/get-involved.png create mode 100644 public/images/language-development/github_invertocat_white.svg create mode 100644 public/images/language-development/php-internals.png create mode 100644 public/images/language-development/rfcs.png create mode 100644 public/images/language-development/submit-bug-report.png create mode 100644 public/images/logos/composer.png create mode 100644 public/images/logos/github_invertocat_white.svg create mode 100644 public/images/logos/php-foundation.svg create mode 100644 public/images/vendors/composer.png create mode 100644 public/images/vendors/php-foundation.svg create mode 100644 public/styles/theme-gst.css create mode 100644 src/Releases/VersionLogos.php create mode 100644 src/Themes/FooterRenderer.php diff --git a/include/branch-highlights.inc b/include/branch-highlights.inc new file mode 100644 index 0000000000..4c6ad1e4c1 --- /dev/null +++ b/include/branch-highlights.inc @@ -0,0 +1,495 @@ + [ + 'features' => [ + [ + 'title' => 'URI Extension', + 'about' => 'PHP 8.5 adds a built-in URI extension to parse, normalize, and handle URLs following RFC 3986 and WHATWG URL standards.', + ], + [ + 'title' => 'Pipe Operator', + 'about' => 'The |> operator enables chaining callables left-to-right, passing values smoothly through multiple functions without intermediary variables.', + ], + [ + 'title' => 'Clone With', + 'about' => 'Clone objects and update properties with the new clone() syntax, making the "with-er" pattern simple for readonly classes.', + ], + [ + 'title' => '#[\NoDiscard] Attribute', + 'about' => 'The #[\NoDiscard] attribute warns when a return value isn’t used, helping prevent mistakes and improving overall API safety.', + ], + [ + 'title' => 'Closures and First-Class Callables in Constant Expressions', + 'about' => 'Static closures and first-class callables can now be used in constant expressions, such as attribute parameters.', + ], + [ + 'title' => 'Persistent cURL Share Handles', + 'about' => 'Handles can now be persisted across multiple PHP requests, avoiding the cost of repeated connection initialization to the same hosts.', + ], + ], + ], + '8.4' => [ + 'support_label' => 'Supported', + 'features' => [ + [ + 'title' => 'Property Hooks', + 'short' => 'Property Hooks allow intercepting properties', + ], + [ + 'title' => 'Asymmetric Property Visibility', + 'short' => 'Asymmetric Visibility for get and set', + ], + [ + 'title' => '#[Deprecated] Attribute', + 'short' => '#[Deprecated] attribute signals removal intent', + ], + [ + 'title' => 'Additional Array Functions', + 'short' => 'New array lookup and query options', + ], + ], + ], + '8.3' => [ + 'support_label' => 'Security Support', + 'features' => [ + [ + 'title' => 'Typed Class Constants', + 'short' => 'Class constants can now be typed', + ], + [ + 'title' => 'Dynamic Class Constants', + 'about' => 'Class constants can now be accessed via dynamic calls', + ], + [ + 'title' => 'Readonly Deep Cloning', + 'short' => 'Enhanced deep cloning of readonly instances', + ], + [ + 'title' => 'Randomizer Improvements', + 'short' => 'Generate random strings from provided character sets', + ], + ], + ], + '8.2' => [ + 'support_label' => 'Security Support', + 'features' => [ + [ + 'title' => 'Readonly classes', + 'short' => 'Entire classes can now be marked Readonly', + ], + [ + 'title' => 'Disjunction Normal Form Types', + 'short' => 'Improved type support with Disjunction Normal Forms', + ], + [ + 'title' => 'Improved Standalone Types', + 'short' => 'Null, true and false are now usable as types', + ], + ], + ], + + /* + * The rest of these were AI generated + */ + + '8.1' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Enums', + 'short' => 'Native support for Enumerations', + ], + [ + 'title' => 'Fibers', + 'short' => 'Core support for asynchronous programming', + ], + [ + 'title' => 'Readonly Properties', + 'short' => 'Class properties can be permanently marked readonly', + ], + [ + 'title' => 'First-class Callable Syntax', + 'short' => 'Clean syntax for creating closures from callables', + ], + ], + ], + '8.0' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Named Arguments', + 'short' => 'Pass arguments to functions based on parameter names', + ], + [ + 'title' => 'Attributes', + 'short' => 'Native syntax for structured metadata (annotations)', + ], + [ + 'title' => 'Constructor Property Promotion', + 'short' => 'Shorthand syntax for defining and initializing properties', + ], + [ + 'title' => 'Match Expression', + 'short' => 'Strict, expression-based alternative to switch statements', + ], + ], + ], + '7.4' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Typed Properties', + 'short' => 'Class properties fully support type declarations', + ], + [ + 'title' => 'Arrow Functions', + 'short' => 'Concise syntax for simple one-liner closures', + ], + [ + 'title' => 'Null Coalescing Assignment', + 'short' => 'Assign values quickly using the ??= operator', + ], + [ + 'title' => 'Spread Operator in Arrays', + 'short' => 'Unpack arrays directly inside other arrays using ...', + ], + ], + ], + '7.3' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Flexible Heredoc/Nowdoc', + 'short' => 'Improved formatting and indentation parsing for multi-line strings', + ], + [ + 'title' => 'Trailing Commas', + 'short' => 'Allow trailing commas in function calls', + ], + [ + 'title' => 'Array Destructuring References', + 'short' => 'List destructuring supports assignment by reference', + ], + [ + 'title' => 'is_countable()', + 'short' => 'New function to safely verify if a variable is countable', + ], + ], + ], + '7.2' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Object Typehint', + 'short' => 'Use \'object\' as a formal type declaration', + ], + [ + 'title' => 'Argon2 Hashing', + 'short' => 'Native support for the Argon2 password hashing algorithm', + ], + [ + 'title' => 'Libsodium Integration', + 'short' => 'Modern cryptography via the core Sodium extension', + ], + [ + 'title' => 'Abstract Method Overriding', + 'short' => 'Traits can now be overridden by abstract methods', + ], + ], + ], + '7.1' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Nullable Types', + 'short' => 'Prepend types with a question mark to allow nulls', + ], + [ + 'title' => 'Void Return Type', + 'short' => 'Functions can explicitly declare a \'void\' return', + ], + [ + 'title' => 'Iterable Pseudo-type', + 'short' => 'Accept arrays or Traversable objects interchangeably', + ], + [ + 'title' => 'Multi-catch Exceptions', + 'short' => 'Catch multiple exception types within a single block', + ], + ], + ], + '7.0' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Scalar Type Declarations', + 'short' => 'Type hinting for int, float, string, and bool', + ], + [ + 'title' => 'Return Type Declarations', + 'short' => 'Specify the strict return type of a function', + ], + [ + 'title' => 'Null Coalescing Operator', + 'short' => 'Simplify isset checks with the ?? operator', + ], + [ + 'title' => 'Spaceship Operator', + 'short' => 'Combined comparison using the <=> operator', + ], + ], + ], + '5.6' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Variadic Functions', + 'short' => 'Accept variable-length argument lists using ...', + ], + [ + 'title' => 'Argument Unpacking', + 'short' => 'Unpack arrays dynamically into function arguments', + ], + [ + 'title' => 'Constant Scalar Expressions', + 'short' => 'Use basic math and expressions when defining constants', + ], + [ + 'title' => 'Exponentiation Operator', + 'short' => 'Calculate powers easily using the ** operator', + ], + ], + ], + '5.5' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Generators', + 'short' => 'Create simple iterators using the \'yield\' keyword', + ], + [ + 'title' => 'Finally Keyword', + 'short' => 'Execute code unconditionally after try/catch blocks', + ], + [ + 'title' => '::class Resolution', + 'short' => 'Fetch fully qualified class names as strings', + ], + [ + 'title' => 'empty() Expressions', + 'short' => 'The empty() construct now accepts arbitrary expressions', + ], + ], + ], + '5.4' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Traits', + 'short' => 'Enable horizontal code reuse across independent classes', + ], + [ + 'title' => 'Short Array Syntax', + 'short' => 'Define arrays cleanly using the [] syntax', + ], + [ + 'title' => 'Built-in Web Server', + 'short' => 'Integrated CLI web server for local development', + ], + [ + 'title' => 'Closure $this', + 'short' => 'Anonymous functions can automatically access object scope', + ], + ], + ], + '5.3' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Namespaces', + 'short' => 'Organize code and prevent naming collisions', + ], + [ + 'title' => 'Late Static Binding', + 'short' => 'Reference the called class context using static::', + ], + [ + 'title' => 'Closures', + 'short' => 'Support for anonymous functions and inline callbacks', + ], + [ + 'title' => 'Ternary Shortcut', + 'short' => 'Omit the middle expression using the ?: operator', + ], + ], + ], + '5.2' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'JSON Support', + 'short' => 'Native json_encode and json_decode functions added', + ], + [ + 'title' => 'Filter Extension', + 'short' => 'Built-in data validation and sanitization filters', + ], + [ + 'title' => 'Zip Extension', + 'short' => 'Native support for creating and reading ZIP archives', + ], + [ + 'title' => 'DateTime Extension', + 'short' => 'Introduction of the object-oriented DateTime class', + ], + ], + ], + '5.1' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'PDO Extension', + 'short' => 'Standardized, lightweight interface for database access', + ], + [ + 'title' => 'Performance Upgrades', + 'short' => 'Significant execution speed improvements over 5.0', + ], + [ + 'title' => 'Magic Methods', + 'short' => 'Added support for __isset() and __unset()', + ], + [ + 'title' => 'Type Hinting for Arrays', + 'short' => 'Allow \'array\' as an explicit parameter type hint', + ], + ], + ], + '5.0' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Zend Engine 2', + 'short' => 'A completely rewritten core engine for better performance', + ], + [ + 'title' => 'Robust Object Model', + 'short' => 'Introduction of true OOP with visibility (public/protected/private)', + ], + [ + 'title' => 'Exceptions', + 'short' => 'Standardized try/catch error handling model', + ], + [ + 'title' => 'SimpleXML', + 'short' => 'An easy-to-use extension for parsing XML structures', + ], + ], + ], + '4.4' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Reference Memory Fixes', + 'short' => 'Fixed major memory corruption issues related to references', + ], + [ + 'title' => 'max_input_nesting_level', + 'short' => 'Added an INI directive to limit the nesting level of input variables', + ], + [ + 'title' => 'Stream-based Hashing', + 'short' => 'File hashing functions now use streams instead of low-level IO', + ], + [ + 'title' => 'SORT_LOCALE_STRING', + 'short' => 'Added flag to sort arrays based on the current locale', + ], + ], + ], + '4.3' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Command Line Interface', + 'short' => 'Introduced a separate CLI SAPI for developing shell applications', + ], + [ + 'title' => 'Streams API', + 'short' => 'Unified approach to handling files, pipes, sockets, and I/O resources', + ], + [ + 'title' => 'Bundled GD Library', + 'short' => 'The GD image manipulation library is now bundled by default', + ], + [ + 'title' => 'New Build System', + 'short' => 'A more portable and less resource-consuming build process', + ], + ], + ], + '4.2' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'register_globals Off', + 'short' => 'External variables are no longer registered in the global scope by default', + ], + [ + 'title' => 'Sockets Overhaul', + 'short' => 'Major overhaul of the sockets extension for better reliability', + ], + [ + 'title' => 'File Upload Performance', + 'short' => 'Highly improved performance and handling of file uploads', + ], + [ + 'title' => 'Apache 2 Support', + 'short' => 'Introduced experimental support for the Apache 2 web server', + ], + ], + ], + '4.1' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Superglobals', + 'short' => 'Introduced autoglobal arrays like $_GET, $_POST, and $_SESSION', + ], + [ + 'title' => 'Windows Performance', + 'short' => 'Revolutionary performance and stability improvements under Windows', + ], + [ + 'title' => 'Output Compression', + 'short' => 'Added turn-key output compression support', + ], + [ + 'title' => 'Extension Versioning', + 'short' => 'Added infrastructure to support version numbers for different extensions', + ], + ], + ], + '4.0' => [ + 'support_label' => 'End of Life', + 'features' => [ + [ + 'title' => 'Zend Engine', + 'short' => 'A new, highly optimized two-stage parse and execute engine', + ], + [ + 'title' => 'Native Sessions', + 'short' => 'Built-in support for HTTP session management', + ], + [ + 'title' => 'Output Buffering', + 'short' => 'Ability to buffer output before sending it to the browser', + ], + [ + 'title' => 'Enhanced OOP', + 'short' => 'More comprehensive object-oriented programming support over PHP 3', + ], + ], + ], +]; diff --git a/include/footer.inc b/include/footer.inc index faa7305d68..75d38174c4 100644 --- a/include/footer.inc +++ b/include/footer.inc @@ -1,4 +1,10 @@ - + + + + + "; @@ -59,7 +65,11 @@ - + +
+ +
+