diff --git a/composer.json b/composer.json index a74b1fe35e5..cce015b27ae 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ ], "require": { "php": "^8.1", + "composer-runtime-api": "^2.0", "composer/installers": "^2.2", "guzzlehttp/guzzle": "^7.5.0", "guzzlehttp/psr7": "^2.4.0", @@ -59,6 +60,7 @@ "ext-xml": "*" }, "require-dev": { + "composer/semver": "^3.4", "phpunit/phpunit": "^9.6", "silverstripe/versioned": "^2", "squizlabs/php_codesniffer": "^3.7" diff --git a/src/Core/Manifest/VersionProvider.php b/src/Core/Manifest/VersionProvider.php index 75339b4ba56..65cfc73768f 100644 --- a/src/Core/Manifest/VersionProvider.php +++ b/src/Core/Manifest/VersionProvider.php @@ -3,11 +3,13 @@ namespace SilverStripe\Core\Manifest; use InvalidArgumentException; -use SilverStripe\Core\Config\Config; +use Composer\InstalledVersions; +use SilverStripe\Dev\Deprecation; use Psr\SimpleCache\CacheInterface; +use SilverStripe\Core\Config\Config; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Injector\Injectable; -use SilverStripe\Core\Injector\Injector; /** * The version provider will look up configured modules and examine the composer.lock file @@ -30,7 +32,7 @@ class VersionProvider use Injectable; /** - * @var array + * @var array */ private static $modules = [ 'silverstripe/framework' => 'Framework', @@ -50,10 +52,10 @@ public function getVersion() return $version; } $modules = $this->getModules(); - $lockModules = $this->getModuleVersionFromComposer(array_keys($modules ?? [])); + $lockModules = $this->getModuleVersionFromComposer(array_keys($modules)); $moduleVersions = []; foreach ($modules as $module => $title) { - if (!array_key_exists($module, $lockModules ?? [])) { + if (!array_key_exists($module, $lockModules)) { continue; } $version = $lockModules[$module]; @@ -145,14 +147,14 @@ private function setCacheValue(string $key, string $value): void * cwp/cwp-core => ['CWP', '4.4.4'] * ] * - * @param array $modules - * @return array + * @param array> $modules + * @return array> */ private function filterModules(array $modules) { $accountModule = []; foreach ($modules as $module => $value) { - if (!preg_match('#^([a-z0-9\-]+)/([a-z0-9\-]+)$#', $module ?? '', $m)) { + if (!preg_match('#^([a-z0-9\-]+)/([a-z0-9\-]+)$#', $module, $m)) { continue; } $account = $m[1]; @@ -169,7 +171,7 @@ private function filterModules(array $modules) /** * Gets the configured core modules to use for the SilverStripe application version * - * @return array + * @return array */ public function getModules() { @@ -180,19 +182,14 @@ public function getModules() /** * Tries to obtain version number from composer.lock if it exists * - * @param array $modules - * @return array + * @param array $modules + * @return array */ public function getModuleVersionFromComposer($modules = []) { $versions = []; - $lockData = $this->getComposerLock(); - if ($lockData && !empty($lockData['packages'])) { - foreach ($lockData['packages'] as $package) { - if (in_array($package['name'], $modules ?? []) && isset($package['version'])) { - $versions[$package['name']] = $package['version']; - } - } + foreach ($modules as $module) { + $versions[$module] = InstalledVersions::getPrettyVersion($module); } return $versions; } @@ -200,35 +197,40 @@ public function getModuleVersionFromComposer($modules = []) /** * Load composer.lock's contents and return it * + * @deprecated 5.1 Has been replaced by composer-runtime-api * @param bool $cache * @return array */ protected function getComposerLock($cache = true) { + Deprecation::notice("5.1", "Has been replaced by composer-runtime-api", Deprecation::SCOPE_METHOD); $composerLockPath = $this->getComposerLockPath(); - if (!file_exists($composerLockPath ?? '')) { + if (!file_exists($composerLockPath)) { return []; } $lockData = []; - $jsonData = file_get_contents($composerLockPath ?? ''); + $jsonData = file_get_contents($composerLockPath); + $jsonData = $jsonData ? $jsonData : ''; + $cacheKey = md5($jsonData); if ($cache) { $cache = Injector::inst()->get(CacheInterface::class . '.VersionProvider_composerlock'); - $cacheKey = md5($jsonData ?? ''); if ($versions = $cache->get($cacheKey)) { - $lockData = json_decode($versions ?? '', true); + $lockData = json_decode($versions, true); } } if (empty($lockData) && $jsonData) { - $lockData = json_decode($jsonData ?? '', true); + $lockData = json_decode($jsonData, true); if ($cache) { $cache->set($cacheKey, $jsonData); } } + $lockData = $lockData ? $lockData : []; + return $lockData; } diff --git a/tests/php/Core/Manifest/VersionProviderTest.php b/tests/php/Core/Manifest/VersionProviderTest.php index 21f07b10f70..96e62795afa 100644 --- a/tests/php/Core/Manifest/VersionProviderTest.php +++ b/tests/php/Core/Manifest/VersionProviderTest.php @@ -2,15 +2,16 @@ namespace SilverStripe\Core\Tests\Manifest; +use SebastianBergmann\Version; +use Composer\Semver\VersionParser; +use SilverStripe\Dev\SapphireTest; use Psr\SimpleCache\CacheInterface; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Manifest\VersionProvider; -use SilverStripe\Dev\SapphireTest; class VersionProviderTest extends SapphireTest { - /** * @var VersionProvider */ @@ -22,17 +23,9 @@ protected function setup(): void $this->clearCache(); } - public function getMockProvider($composerLockPath = '') + public function getProvider() { - if ($composerLockPath == '') { - // composer.lock file without silverstripe/recipe-core or silverstripe/recipe-cms - $composerLockPath = __DIR__ . '/fixtures/VersionProviderTest/composer.no-recipe.testlock'; - } - /** @var VersionProvider $provider */ - $provider = $this->getMockBuilder(VersionProvider::class) - ->setMethods(['getComposerLockPath']) - ->getMock(); - $provider->method('getComposerLockPath')->willReturn($composerLockPath); + $provider = Injector::inst()->get(VersionProvider::class); return $provider; } @@ -44,7 +37,7 @@ public function testGetModules() 'silverstripe/another' => 'Another', 'cwp/cwp-something' => 'CWP something', ]); - $result = $this->getMockProvider()->getModules(); + $result = $this->getProvider()->getModules(); $this->assertArrayHasKey('silverstripe/mypackage', $result); $this->assertArrayHasKey('silverstripe/somepackage', $result); $this->assertArrayHasKey('silverstripe/another', $result); @@ -56,7 +49,7 @@ public function testGetModulesEmpty() Config::modify()->set(VersionProvider::class, 'modules', []); $this->assertEquals( ['silverstripe/framework' => 'Framework'], - $this->getMockProvider()->getModules() + $this->getProvider()->getModules() ); } @@ -65,7 +58,7 @@ public function testGetModulesNone() Config::modify()->remove(VersionProvider::class, 'modules'); $this->assertEquals( ['silverstripe/framework' => 'Framework'], - $this->getMockProvider()->getModules() + $this->getProvider()->getModules() ); } @@ -76,7 +69,7 @@ public function testGetModuleVersionFromComposer() 'silverstripe/framework' => 'Framework', ]); - $result = $this->getMockProvider()->getModules(['silverstripe/framework']); + $result = $this->getProvider()->getModules(['silverstripe/framework']); $this->assertArrayHasKey('silverstripe/framework', $result); $this->assertNotEmpty($result['silverstripe/framework']); } @@ -87,131 +80,23 @@ public function testGetVersion() 'silverstripe/siteconfig' => 'SiteConfig', 'silverstripe/framework' => 'Framework' ]); - $result = $this->getMockProvider()->getVersion(); + $result = $this->getProvider()->getVersion(); $this->assertStringNotContainsString('SiteConfig: ', $result); $this->assertStringContainsString('Framework: ', $result); $this->assertStringNotContainsString(', ', $result); } - public function testGetVersionNoRecipe() - { - // composer.lock file without silverstripe/recipe-core or silverstripe/recipe-cms - $provider = $this->getMockProvider(__DIR__ . '/fixtures/VersionProviderTest/composer.no-recipe.testlock'); - - Config::modify()->set(VersionProvider::class, 'modules', []); - $result = $provider->getVersion(); - $this->assertStringContainsString('Framework: 1.2.3', $result); - - $this->clearCache(); - - Config::modify()->set(VersionProvider::class, 'modules', [ - 'silverstripe/framework' => 'Framework', - 'silverstripe/recipe-core' => 'Core Recipe', - 'silverstripe/cms' => 'CMS', - 'silverstripe/recipe-cms' => 'CMS Recipe', - ]); - $result = $provider->getVersion(); - $this->assertStringNotContainsString('Framework: 1.2.3', $result); - $this->assertStringContainsString('CMS: 4.5.6', $result); - $this->assertStringNotContainsString('Core Recipe: 7.7.7', $result); - $this->assertStringNotContainsString('CMS Recipe: 8.8.8', $result); - } - - public function testGetVersionRecipeCore() - { - // composer.lock file with silverstripe/recipe-core but not silverstripe/recipe-cms - $provider = $this->getMockProvider(__DIR__ . '/fixtures/VersionProviderTest/composer.recipe-core.testlock'); - Config::modify()->set(VersionProvider::class, 'modules', [ - 'silverstripe/framework' => 'Framework', - 'silverstripe/recipe-core' => 'Core Recipe', - 'silverstripe/cms' => 'CMS', - 'silverstripe/recipe-cms' => 'CMS Recipe', - ]); - $result = $provider->getVersion(); - $this->assertStringNotContainsString('Framework: 1.2.3', $result); - $this->assertStringNotContainsString('Core Recipe: 7.7.7', $result); - $this->assertStringContainsString('CMS: 4.5.6', $result); - $this->assertStringNotContainsString('CMS Recipe: 8.8.8', $result); - } - - public function testGetVersionRecipeCmsCore() - { - // composer.lock file with silverstripe/recipe-core and silverstripe/recipe-cms - $path = __DIR__ . '/fixtures/VersionProviderTest/composer.recipe-cms-core-and-cwpcore.testlock'; - $provider = $this->getMockProvider($path); - - Config::modify()->set(VersionProvider::class, 'modules', [ - 'silverstripe/framework' => 'Framework', - 'silverstripe/recipe-core' => 'Core Recipe', - 'silverstripe/cms' => 'CMS', - 'silverstripe/recipe-cms' => 'CMS Recipe', - ]); - $result = $provider->getVersion(); - - $this->assertStringNotContainsString('Framework: 1.2.3', $result); - $this->assertStringNotContainsString('CMS: 4.5.6', $result); - $this->assertStringNotContainsString('Core Recipe: 7.7.7', $result); - $this->assertStringContainsString('CMS Recipe: 8.8.8', $result); - $this->assertStringNotContainsString('CWP: 9.9.9', $result); - - $this->clearCache(); - - Config::modify()->set(VersionProvider::class, 'modules', [ - 'silverstripe/framework' => 'Framework', - 'silverstripe/recipe-core' => 'Core Recipe', - 'silverstripe/cms' => 'CMS', - 'silverstripe/recipe-cms' => 'CMS Recipe', - 'cwp/cwp-core' => 'CWP', - ]); - $result = $provider->getVersion(); - $this->assertStringNotContainsString('Framework: 1.2.3', $result); - $this->assertStringNotContainsString('CMS: 4.5.6', $result); - $this->assertStringNotContainsString('Core Recipe: 7.7.7', $result); - $this->assertStringContainsString('CMS Recipe:', $result); - $this->assertStringContainsString('CWP: 9.9.9', $result); - } - - public function testGetModulesFromComposerLock() - { - $mock = $this->getMockBuilder(VersionProvider::class) - ->setMethods(['getComposerLock']) - ->getMock(); - - $mock->expects($this->exactly(1)) - ->method('getComposerLock') - ->will($this->returnValue([ - 'packages' => [ - [ - 'name' => 'silverstripe/somepackage', - 'version' => '1.2.3' - ], - [ - 'name' => 'silverstripe/another', - 'version' => '2.3.4' - ] - ] - ])); - - Config::modify()->set(VersionProvider::class, 'modules', [ - 'silverstripe/somepackage' => 'Some Package' - ]); - - $result = $mock->getVersion(); - $this->assertStringContainsString('Some Package: 1.2.3', $result); - } - public function testGetModuleVersion() { - $provider = $this->getMockProvider(__DIR__ . '/fixtures/VersionProviderTest/composer.recipe-core.testlock'); + $provider = $this->getProvider(); Config::modify()->set(VersionProvider::class, 'modules', [ 'silverstripe/framework' => 'Framework', - 'silverstripe/recipe-core' => 'Core Recipe' ]); - $this->assertSame('1.2.3', $provider->getModuleVersion('silverstripe/framework')); - // assert that the temporary config changes in getModuleVersion() had no side-effects + $moduleVersion = $provider->getModuleVersion('silverstripe/framework'); + $parser = new VersionParser(); + $this->assertIsString($parser->normalize($moduleVersion), "Expected a valid semver but got $moduleVersion"); $result = $provider->getVersion(); $this->assertStringNotContainsString('Framework: 1.2.3', $result); - $this->assertStringContainsString('Core Recipe: 7.7.7', $result); } private function clearCache() diff --git a/tests/php/Core/Manifest/fixtures/VersionProviderTest/composer.no-recipe.testlock b/tests/php/Core/Manifest/fixtures/VersionProviderTest/composer.no-recipe.testlock deleted file mode 100644 index aad7303d9b0..00000000000 --- a/tests/php/Core/Manifest/fixtures/VersionProviderTest/composer.no-recipe.testlock +++ /dev/null @@ -1,16 +0,0 @@ -{ - "_readme": [ - "This is a fixture file for unit tests" - ], - "content-hash": "abc123", - "packages": [ - { - "name": "silverstripe/framework", - "version": "1.2.3" - }, - { - "name": "silverstripe/cms", - "version": "4.5.6" - } - ] -} diff --git a/tests/php/Core/Manifest/fixtures/VersionProviderTest/composer.recipe-cms-core-and-cwpcore.testlock b/tests/php/Core/Manifest/fixtures/VersionProviderTest/composer.recipe-cms-core-and-cwpcore.testlock deleted file mode 100644 index a3beb11f0c6..00000000000 --- a/tests/php/Core/Manifest/fixtures/VersionProviderTest/composer.recipe-cms-core-and-cwpcore.testlock +++ /dev/null @@ -1,28 +0,0 @@ -{ - "_readme": [ - "This is a fixture file for unit tests" - ], - "content-hash": "abc888", - "packages": [ - { - "name": "silverstripe/framework", - "version": "1.2.3" - }, - { - "name": "silverstripe/cms", - "version": "4.5.6" - }, - { - "name": "silverstripe/recipe-core", - "version": "7.7.7" - }, - { - "name": "silverstripe/recipe-cms", - "version": "8.8.8" - }, - { - "name": "cwp/cwp-core", - "version": "9.9.9" - } - ] -} diff --git a/tests/php/Core/Manifest/fixtures/VersionProviderTest/composer.recipe-core.testlock b/tests/php/Core/Manifest/fixtures/VersionProviderTest/composer.recipe-core.testlock deleted file mode 100644 index 4cf00ce0cf0..00000000000 --- a/tests/php/Core/Manifest/fixtures/VersionProviderTest/composer.recipe-core.testlock +++ /dev/null @@ -1,20 +0,0 @@ -{ - "_readme": [ - "This is a fixture file for unit tests" - ], - "content-hash": "abc777", - "packages": [ - { - "name": "silverstripe/framework", - "version": "1.2.3" - }, - { - "name": "silverstripe/cms", - "version": "4.5.6" - }, - { - "name": "silverstripe/recipe-core", - "version": "7.7.7" - } - ] -}