From 0699bd1c5b8b4420881ce5c2408d3e759b626c1d Mon Sep 17 00:00:00 2001 From: George Steel Date: Wed, 7 Oct 2026 22:48:24 +0100 Subject: [PATCH] Fix: Process all standalone tools during 8.6 migration Instead of patching composer with regex (In an attempt to reduce the size of diffs), we now manipulate the Json using a round trip via json_encode. This makes it easier to re-use the `ComposerFile` class for the composer files in all the possible tooling directories. Signed-off-by: George Steel --- migrations/PHP-86/migrate | 32 ++++- migrations/src/ComposerFile.php | 89 ++++++++++-- migrations/src/PatchComposer.php | 83 ------------ migrations/test/ComposerFileTest.php | 187 ++++++++++++++++++++++++++ migrations/test/PatchComposerTest.php | 154 --------------------- 5 files changed, 294 insertions(+), 251 deletions(-) delete mode 100644 migrations/src/PatchComposer.php create mode 100644 migrations/test/ComposerFileTest.php delete mode 100644 migrations/test/PatchComposerTest.php diff --git a/migrations/PHP-86/migrate b/migrations/PHP-86/migrate index 7a4fea8..ccedb5f 100644 --- a/migrations/PHP-86/migrate +++ b/migrations/PHP-86/migrate @@ -3,11 +3,11 @@ declare(strict_types=1); +use Laminas\Internal\Migrations\ComposerFile; use Laminas\Internal\Migrations\LaminasCI; use Laminas\Internal\Migrations\LaminasCIWorkflow; use Laminas\Internal\Migrations\MagoConfig; use Laminas\Internal\Migrations\Makefile; -use Laminas\Internal\Migrations\PatchComposer; use Laminas\Internal\Migrations\ProjectInformation; require __DIR__ . '/../autoloader.php'; @@ -25,10 +25,10 @@ if ( $project = ProjectInformation::fromDirectory($rootDirectory); // Patch Composer -(new PatchComposer($project->composerFile))( - '8.3.99', - '~8.3 || ~8.4 || ~8.5 || ~8.6', -); +$composer = new ComposerFile($project->composerFile); +$composer->setPlatform('8.3.99'); +$composer->setPhpVersionConstraint('~8.3 || ~8.4 || ~8.5 || ~8.6'); +$composer->write(); // Enable BC Checker and Ignore Platform Reqs on 8.6 if ($project->laminasCiConfig !== null) { @@ -74,3 +74,25 @@ if ($project->makefile !== null) { $makefile = new Makefile($project->makefile); $makefile->updatePHPVersionTo('8.3'); } + +// Update the composer files for anything in the tools directory +$toolsDirectory = $project->projectDirectory . '/tools'; +if (is_dir($toolsDirectory) && file_exists($toolsDirectory)) { + $iterator = new DirectoryIterator($toolsDirectory); + /** @var SplFileInfo&DirectoryIterator $item */ + foreach ($iterator as $item) { + if ($item->isDot() || ! $item->isDir()) { + continue; + } + + $file = $item->getRealPath() . '/composer.json'; + if (! file_exists($file)) { + continue; + } + + $composer = new ComposerFile($file); + $composer->setPlatform('8.3.99'); + $composer->setPhpVersionConstraint('~8.3 || ~8.4 || ~8.5 || ~8.6'); + $composer->write(); + } +} diff --git a/migrations/src/ComposerFile.php b/migrations/src/ComposerFile.php index 8dbee11..6b011ee 100644 --- a/migrations/src/ComposerFile.php +++ b/migrations/src/ComposerFile.php @@ -5,29 +5,46 @@ namespace Laminas\Internal\Migrations; use Composer\Semver\VersionParser; +use JsonException; +use RuntimeException; +use Throwable; use function array_slice; use function assert; use function count; use function explode; use function file_get_contents; +use function file_put_contents; use function implode; use function json_decode; +use function json_encode; -final readonly class ComposerFile +use const JSON_PRETTY_PRINT; +use const JSON_THROW_ON_ERROR; + +final class ComposerFile { /** * @var array{ + * config?: array{ + * platform?: array{ + * php?: non-empty-string, + * }, + * }, * require: array{ - * php: non-empty-string, - * } + * php?: non-empty-string, + * }, * } */ private array $data; + private bool $dirty = false; - /** @param non-empty-string $file */ + /** + * @param non-empty-string $file + * @throws JsonException + */ public function __construct( - private string $file, + private readonly string $file, ) { /** @mago-expect analysis:mixed-property-type-coercion(1) */ $this->data = json_decode( @@ -36,7 +53,10 @@ public function __construct( ); } - /** @return non-empty-string */ + /** + * @return non-empty-string + * @throws Throwable + */ public function minimumPHPVersionAsMinor(): string { $parser = new VersionParser(); @@ -50,13 +70,48 @@ public function minimumPHPVersionAsMinor(): string return $minor; } - /** @return non-empty-string */ + /** + * @return non-empty-string + * @throws RuntimeException + */ public function phpVersionConstraint(): string { - return $this->data['require']['php']; + $version = $this->data['require']['php'] ?? null; + if ($version === null) { + throw new RuntimeException('This composer file does not declare a PHP version constraint'); + } + + return $version; + } + + /** @param non-empty-string $constraint */ + public function setPhpVersionConstraint(string $constraint): void + { + if (($this->data['require']['php'] ?? null) === $constraint) { + return; + } + + $this->data['require']['php'] = $constraint; + $this->dirty = true; } - /** @return non-empty-string */ + /** @param non-empty-string $version */ + public function setPlatform(string $version): void + { + $this->data['config'] ??= []; + $this->data['config']['platform'] ??= []; + if (($this->data['config']['platform']['php'] ?? null) === $version) { + return; + } + + $this->data['config']['platform']['php'] = $version; + $this->dirty = true; + } + + /** + * @return non-empty-string + * @throws JsonException + */ private function read(): string { $contents = file_get_contents($this->file); @@ -67,4 +122,20 @@ private function read(): string return $contents; } + + /** @throws JsonException */ + public function write(): void + { + if (! $this->dirty) { + return; + } + + file_put_contents( + $this->file, + json_encode( + $this->data, + JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT, + ), + ); + } } diff --git a/migrations/src/PatchComposer.php b/migrations/src/PatchComposer.php deleted file mode 100644 index f9b0d86..0000000 --- a/migrations/src/PatchComposer.php +++ /dev/null @@ -1,83 +0,0 @@ -read(); - $source = $this->platformUpdate($source, $platform); - $source = $this->phpConstraints($source, $phpConstraint); - - $this->write($source); - } - - /** - * @param non-empty-string $data - * @param non-empty-string $platform - * @return non-empty-string - */ - private function platformUpdate(string $data, string $platform): string - { - $result = preg_replace( - '/("platform"\s*:\s*{.+)("php"\s*:\s*"8.[0-9]+.99")/mis', - '$1"php": "' . $platform . '"', - $data, - ); - assert($result !== '' && $result !== null, 'Unexpected Result'); - - return $result; - } - - /** - * @param non-empty-string $data - * @param non-empty-string $target - * @return non-empty-string - */ - private function phpConstraints(string $data, string $target): string - { - $result = preg_replace( - '/("require"\s*:\s*{.+)("php"\s*:\s*"[~0-9.><=|^\s]+")/mis', - '$1"php": "' . $target . '"', - $data, - ); - assert($result !== '' && $result !== null, 'Unexpected result'); - - return $result; - } - - /** @return non-empty-string */ - private function read(): string - { - $contents = file_get_contents($this->file); - assert( - $contents !== false && $contents !== '', - 'composer.json could not be read or it is empty', - ); - - return $contents; - } - - /** @param non-empty-string $data */ - private function write(string $data): void - { - file_put_contents($this->file, $data); - } -} diff --git a/migrations/test/ComposerFileTest.php b/migrations/test/ComposerFileTest.php new file mode 100644 index 0000000..aa243d2 --- /dev/null +++ b/migrations/test/ComposerFileTest.php @@ -0,0 +1,187 @@ + + */ + public static function changePHPConstraintProvider(): iterable + { + yield 'No changes given identical version' => [ + <<= 6.0" + } + } + JSON, + <<= 6.0" + } + } + JSON, + '>= 6.0', + ]; + + yield 'Constraint can be changed' => [ + <<= 6.0" + } + } + JSON, + << [ + <<= 6.0" + } + } + JSON, + <<= 6.0", + "php": "~8.5" + } + } + JSON, + '~8.5', + ]; + } + + /** + * @param non-empty-string $source + * @param non-empty-string $expect + * @param non-empty-string $version + */ + #[DataProvider('changePHPConstraintProvider')] + public function testAlterationOfPHPConstraint(string $source, string $expect, string $version): void + { + $file = TestHelper::writeToTempFile($source); + $composer = new ComposerFile($file); + $composer->setPhpVersionConstraint($version); + $composer->write(); + self::assertJsonStringEqualsJsonFile($file, $expect); + self::assertSame($version, $composer->phpVersionConstraint()); + } + + public function testExceptionThrownRetrievingPhpVersionWhenNotKnown(): void + { + $file = TestHelper::writeToTempFile('{}'); + $composer = new ComposerFile($file); + $this->expectException(RuntimeException::class); + $composer->phpVersionConstraint(); + } + + /** + * @return iterable + */ + public static function minimumVersionProvider(): iterable + { + yield [ + '{"require":{"php":"^8.3.45"}}', + '8.3', + ]; + + yield [ + '{"require":{"php":"~5.6 || ~7.1"}}', + '5.6', + ]; + + yield [ + '{"require":{"php":"~7.1 || ~5.6"}}', + '5.6', + ]; + + yield [ + '{"require":{"php":"8.6.0-dev"}}', + '8.6', + ]; + } + + /** + * @param non-empty-string $source + * @param non-empty-string $expect + */ + #[DataProvider('minimumVersionProvider')] + public function testMinimumPhpVersion(string $source, string $expect): void + { + $file = TestHelper::writeToTempFile($source); + $composer = new ComposerFile($file); + + self::assertSame($expect, $composer->minimumPHPVersionAsMinor()); + } + + /** + * @return iterable + */ + public static function platformProvider(): iterable + { + yield 'Is set when not present' => [ + '{}', + '{"config":{"platform":{"php":"1.2.33"}}}', + '1.2.33', + ]; + + yield 'Is mutated' => [ + '{"config":{"platform":{"php":"1.2.33"}}}', + '{"config":{"platform":{"php":"4.0"}}}', + '4.0', + ]; + + yield 'Identical' => [ + '{"config":{"platform":{"php":"4.0"}}}', + '{"config":{"platform":{"php":"4.0"}}}', + '4.0', + ]; + } + + /** + * @param non-empty-string $source + * @param non-empty-string $expect + * @param non-empty-string $newVersion + */ + #[DataProvider('platformProvider')] + public function testChangingPlatforms(string $source, string $expect, string $newVersion): void + { + $file = TestHelper::writeToTempFile($source); + $composer = new ComposerFile($file); + $composer->setPlatform($newVersion); + $composer->write(); + self::assertJsonStringEqualsJsonFile($file, $expect); + } +} diff --git a/migrations/test/PatchComposerTest.php b/migrations/test/PatchComposerTest.php deleted file mode 100644 index 161509a..0000000 --- a/migrations/test/PatchComposerTest.php +++ /dev/null @@ -1,154 +0,0 @@ - - */ - public static function dataProvider(): iterable - { - yield [ - <<= 8.9", - "after/after": "^1.51.2" - } - } - JSON, - '8.9.99', - '>= 8.9', - ]; - yield [ - <<= 7.4", - "after/after": "^1.51.2" - } - } - JSON, - <<__invoke($platform, $phpConstraints); - - self::assertEquals(file_get_contents($path), $expect); - } -}