1<?php 2 3namespace App\Utils\Versions; 4 5use App\Utils\Cache; 6 7/** 8 * Service for retrieving a list of valid PHP versions when reporting bugs. PHP 9 * versions have format MAJOR.MINOR.MICRO{TYPE} where TYPE is one of alpha, 10 * beta, RC, or dev. 11 * 12 * Stable releases are pulled from the https://php.net. The RC and dev versions 13 * are pulled from the https://qa.php.net. 14 * 15 * To add a new PHP version add it to: 16 * https://github.com/php/web-qa/blob/master/include/release-qa.php 17 * 18 * The versions are weighted by the following criteria: 19 * - major+minor version desc (7>5.4>5.3>master) 20 * - Between minor versions ordering is done by the micro version if available. 21 * First the QA releases: alpha/beta/rc, then stable, then nightly versions 22 * (Git, snaps). Snaps are more or less Windows snapshot builds. 23 * 24 * The result is cached for 1 hour into a temporary file. 25 */ 26class Generator 27{ 28 /** 29 * PHP API pages client. 30 * 31 * @var Client 32 */ 33 private $client; 34 35 /** 36 * Cache service for storing fetched versions. 37 * 38 * @var Cache 39 */ 40 private $cache; 41 42 /** 43 * Time after cache file is considered expired in seconds. 44 */ 45 private const TTL = 3600; 46 47 /** 48 * Additional versions appended to the list of generated versions. 49 */ 50 private const APPENDICES = [ 51 'Next Major Version', 52 'Next Minor Version', 53 'Irrelevant', 54 ]; 55 56 /** 57 * Class constructor. 58 */ 59 public function __construct(Client $client, Cache $cache) 60 { 61 $this->client = $client; 62 $this->cache = $cache; 63 } 64 65 /** 66 * Get a list of valid PHP versions. Versions are cached for efficiency. 67 */ 68 public function getVersions(): array 69 { 70 if (!$this->cache->has('versions')) { 71 $this->cache->set('versions', $this->generateVersions(), self::TTL); 72 } 73 74 return $this->cache->get('versions'); 75 } 76 77 /** 78 * Return fetched and processed versions. 79 */ 80 private function generateVersions(): array 81 { 82 $versions = array_merge($this->getDevVersions(), $this->getStableVersions()); 83 rsort($versions); 84 85 // Get minor branches (PHP 7.2, PHP 7.3, etc) 86 $branches = []; 87 foreach ($versions as $version) { 88 $parts = $this->parseVersion($version); 89 $branch = $parts['major'].'.'.$parts['minor']; 90 $branches[$branch] = $branch; 91 } 92 93 $sorted = []; 94 95 // Add versions grouped by branches 96 foreach ($branches as $branch) { 97 foreach ($versions as $version) { 98 $parts = $this->parseVersion($version); 99 if ($parts['major'].'.'.$parts['minor'] === $branch) { 100 $sorted[] = $version; 101 } 102 } 103 104 // Append Git and snaps for each branch 105 foreach ($this->getAffixes() as $item) { 106 $sorted[] = $branch.$item; 107 } 108 } 109 110 // Append master branch to the versions list 111 foreach ($this->getAffixes() as $item) { 112 $sorted[] = 'master-'.$item; 113 } 114 115 // Append human readable versions 116 $sorted = array_merge($sorted, self::APPENDICES); 117 118 return $sorted; 119 } 120 121 /** 122 * Get version affixes such as Git or snapshots. 123 */ 124 protected function getAffixes(): array 125 { 126 $date = date('Y-m-d'); 127 128 return [ 129 'Git-'.$date.' (Git)', 130 'Git-'.$date.' (snap)', 131 ]; 132 } 133 134 /** 135 * Get alpha, beta and RC versions. 136 */ 137 private function getDevVersions(): array 138 { 139 $versions = []; 140 141 foreach ($this->client->fetchDevVersions() as $version) { 142 $parts = $this->parseVersion($version); 143 if ('dev' !== $parts['type']) { 144 $versions[] = $version; 145 } 146 } 147 148 return $versions; 149 } 150 151 /** 152 * Get stable versions. 153 */ 154 private function getStableVersions(): array 155 { 156 $versions = []; 157 158 foreach ($this->client->fetchStableVersions() as $releases) { 159 foreach ($releases as $release) { 160 $versions[] = $release['version']; 161 } 162 } 163 164 return $versions; 165 } 166 167 /** 168 * Parse the versions data string and convert it to array. 169 */ 170 private function parseVersion(string $version): array 171 { 172 $matches = []; 173 preg_match('#(?P<major>\d+)\.(?P<minor>\d+).(?P<micro>\d+)[-]?(?P<type>RC|alpha|beta|dev)?(?P<number>[\d]?).*#ui', $version, $matches); 174 $parts = [ 175 'major' => $matches['major'], 176 'minor' => $matches['minor'], 177 'micro' => $matches['micro'], 178 'type' => strtolower($matches['type'] ? $matches['type'] : 'stable'), 179 'number' => $matches['number'], 180 'original' => $version, 181 ]; 182 183 return $parts; 184 } 185} 186