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): ?>
+
+
+
+
+
+
+
+
+ -
+
+
+ ', $rel['md5'], '';
+ if (isset($rel['sha256'])) echo '', $rel['sha256'], '';
+ ?>
+
+
+ Note:
+
+
+
+
+
+ -
+
+ Windows downloads
+
+
+
+
+
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): ?>
-
-
-
-
-
-
-
-
- -
-
-
- ', $rel['md5'], '';
- if (isset($rel['sha256'])) echo '', $rel['sha256'], '';
- ?>
-
-
- Note:
-
-
-
-
-
- -
-
- Windows downloads
-
-
-
-
-
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"