From 2d94786e3727cbc96734b6e4a70c13a6c19d809b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 3 Apr 2026 00:36:29 +0200 Subject: [PATCH 1/2] [DumpEnvCommand] Fix testEnvCanBeReferenced for dotenv deferred resolution symfony/dotenv >= 6.4.35/7.4.7/8.0.7 defers variable resolution so BAR=$FOO now resolves using the .env value of FOO rather than the system env at dump time. --- tests/Command/DumpEnvCommandTest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/Command/DumpEnvCommandTest.php b/tests/Command/DumpEnvCommandTest.php index 0f29a586..8ee9daf4 100644 --- a/tests/Command/DumpEnvCommandTest.php +++ b/tests/Command/DumpEnvCommandTest.php @@ -13,6 +13,8 @@ use Composer\Config; use Composer\Console\Application; +use Composer\InstalledVersions; +use Composer\Semver\VersionParser; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Flex\Command\DumpEnvCommand; @@ -109,10 +111,14 @@ public function testEnvCanBeReferenced() $this->assertFileExists($envLocal); + // With dotenv >= 6.4.35/7.4.7/8.0.7, variable resolution is deferred so BAR=$FOO + // resolves using .env's FOO value. With older versions, eager resolution uses system env. + $deferredResolution = InstalledVersions::satisfies(new VersionParser(), 'symfony/dotenv', '^6.4.35 | ^7.4.7 | >=8.0.7'); + $vars = require $envLocal; $this->assertSame([ 'APP_ENV' => 'prod', - 'BAR' => 'Foo', + 'BAR' => $deferredResolution ? '123' : 'Foo', 'FOO' => '123', ], $vars); From 936194fd2efa09bdebe70bc8eb5b0539e8366e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 3 Apr 2026 00:54:37 +0200 Subject: [PATCH 2/2] [Unpacker] Copy "replace" and "provide" entries when unpacking a pack Fixes #653 --- src/Unpacker.php | 23 +++++++++++-- tests/UnpackerTest.php | 78 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/src/Unpacker.php b/src/Unpacker.php index da9d228d..47998739 100644 --- a/src/Unpacker.php +++ b/src/Unpacker.php @@ -38,7 +38,7 @@ public function __construct(Composer $composer, PackageResolver $resolver) $this->versionParser = new VersionParser(); } - public function unpack(Operation $op, ?Result $result = null, &$links = [], bool $devRequire = false): Result + public function unpack(Operation $op, ?Result $result = null, &$links = [], bool $devRequire = false, &$replaces = [], &$provides = []): Result { if (null === $result) { $result = new Result(); @@ -95,7 +95,7 @@ public function unpack(Operation $op, ?Result $result = null, &$links = [], bool if ('symfony-pack' === $subPkg->getType()) { $subOp = new Operation(true, $op->shouldSort()); $subOp->addPackage($subPkg->getName(), $constraint, $dev); - $result = $this->unpack($subOp, $result, $links, $dev); + $result = $this->unpack($subOp, $result, $links, $dev, $replaces, $provides); continue; } @@ -127,6 +127,13 @@ public function unpack(Operation $op, ?Result $result = null, &$links = [], bool } } } + + foreach ($pkg->getReplaces() as $link) { + $replaces[$link->getTarget()] = $link->getPrettyConstraint(); + } + foreach ($pkg->getProvides() as $link) { + $provides[$link->getTarget()] = $link->getPrettyConstraint(); + } } if (1 < \func_num_args()) { @@ -170,6 +177,18 @@ public function unpack(Operation $op, ?Result $result = null, &$links = [], bool } } + foreach ($replaces as $name => $constraint) { + if (!isset($jsonStored['replace'][$name])) { + $jsonManipulator->addLink('replace', $name, $constraint, $op->shouldSort()); + } + } + + foreach ($provides as $name => $constraint) { + if (!isset($jsonStored['provide'][$name])) { + $jsonManipulator->addLink('provide', $name, $constraint, $op->shouldSort()); + } + } + file_put_contents($jsonPath, $jsonManipulator->getContents()); return $result; diff --git a/tests/UnpackerTest.php b/tests/UnpackerTest.php index 785da05a..75685362 100644 --- a/tests/UnpackerTest.php +++ b/tests/UnpackerTest.php @@ -29,7 +29,7 @@ class UnpackerTest extends TestCase * * - "real" package MUST be present ONLY in "require" section */ - public function testDoNotDuplicateEntry(): void + public function testDoNotDuplicateEntry() { // Setup project @@ -98,4 +98,80 @@ public function testDoNotDuplicateEntry(): void putenv('COMPOSER='.$originalEnvComposer); @unlink($composerJsonPath); } + + /** + * When unpacking a pack, its "replace" and "provide" entries must be + * copied to the root composer.json. + */ + public function testUnpackCopiesReplaceAndProvide() + { + // Setup project + + $composerJsonPath = FLEX_TEST_DIR.'/composer.json'; + + @mkdir(FLEX_TEST_DIR); + @unlink($composerJsonPath); + file_put_contents($composerJsonPath, '{}'); + + $originalEnvComposer = $_SERVER['COMPOSER']; + $_SERVER['COMPOSER'] = $composerJsonPath; + // composer 2.1 and lower support + putenv('COMPOSER='.$composerJsonPath); + + // Setup packages + + $realPkg = new Package('real', '1.0.0', '1.0.0'); + $realPkgLink = new Link('lorem', 'real', new MatchAllConstraint(), 'wraps', '1.0.0'); + + $virtualPkg = new Package('pack_foo', '1.0.0', '1.0.0'); + $virtualPkg->setType('symfony-pack'); + $virtualPkg->setRequires(['real' => $realPkgLink]); + $virtualPkg->setReplaces([ + 'old/package' => new Link('pack_foo', 'old/package', new MatchAllConstraint(), 'replaces', 'self.version'), + ]); + $virtualPkg->setProvides([ + 'some/capability' => new Link('pack_foo', 'some/capability', new MatchAllConstraint(), 'provides', 'self.version'), + ]); + + $packages = [$realPkg, $virtualPkg]; + + // Setup Composer + + $repManager = $this->getMockBuilder(RepositoryManager::class)->disableOriginalConstructor()->getMock(); + $repManager->expects($this->any())->method('getLocalRepository')->willReturn(new InstalledArrayRepository($packages)); + + $composer = new Composer(); + $composer->setRepositoryManager($repManager); + + // Unpack + + $resolver = $this->getMockBuilder(PackageResolver::class)->disableOriginalConstructor()->getMock(); + + $unpacker = new Unpacker($composer, $resolver); + + $operation = new Operation(true, false); + $operation->addPackage('pack_foo', '*', false); + + $unpacker->unpack($operation); + + // Check + + $composerJson = json_decode(file_get_contents($composerJsonPath), true); + + $this->assertArrayHasKey('replace', $composerJson); + $this->assertArrayHasKey('old/package', $composerJson['replace']); + $this->assertArrayHasKey('provide', $composerJson); + $this->assertArrayHasKey('some/capability', $composerJson['provide']); + + // Restore + + if ($originalEnvComposer) { + $_SERVER['COMPOSER'] = $originalEnvComposer; + } else { + unset($_SERVER['COMPOSER']); + } + // composer 2.1 and lower support + putenv('COMPOSER='.$originalEnvComposer); + @unlink($composerJsonPath); + } }