diff --git a/src/Command/InstallExtensionsForProjectCommand.php b/src/Command/InstallExtensionsForProjectCommand.php index cf87564b..e80f0083 100644 --- a/src/Command/InstallExtensionsForProjectCommand.php +++ b/src/Command/InstallExtensionsForProjectCommand.php @@ -11,8 +11,9 @@ use Php\Pie\ComposerIntegration\PieJsonEditor; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\Installing\InstallForPhpProject\ComposerFactoryForProject; +use Php\Pie\Installing\InstallForPhpProject\DetermineExtensionsRequired; use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages; -use Php\Pie\Installing\InstallForPhpProject\FindRootPackage; use Php\Pie\Installing\InstallForPhpProject\InstallPiePackageFromPath; use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage; use Psr\Container\ContainerInterface; @@ -26,7 +27,6 @@ use Symfony\Component\Console\Question\ChoiceQuestion; use Throwable; -use function array_filter; use function array_keys; use function array_map; use function array_merge; @@ -37,8 +37,6 @@ use function is_string; use function realpath; use function sprintf; -use function str_starts_with; -use function strlen; use function strpos; use function substr; @@ -51,7 +49,8 @@ final class InstallExtensionsForProjectCommand extends Command { public function __construct( - private readonly FindRootPackage $findRootPackage, + private readonly ComposerFactoryForProject $composerFactoryForProject, + private readonly DetermineExtensionsRequired $determineExtensionsRequired, private readonly FindMatchingPackages $findMatchingPackages, private readonly InstallSelectedPackage $installSelectedPackage, private readonly InstallPiePackageFromPath $installPiePackageFromPath, @@ -72,7 +71,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $helper = $this->getHelper('question'); assert($helper instanceof QuestionHelper); - $rootPackage = $this->findRootPackage->forCwd($input, $output); + $rootPackage = $this->composerFactoryForProject->rootPackage($input, $output); if (ExtensionType::isValid($rootPackage->getType())) { $cwd = realpath(getcwd()); @@ -100,17 +99,7 @@ public function execute(InputInterface $input, OutputInterface $output): int getcwd(), )); - $rootPackageExtensionsRequired = array_filter( - array_merge($rootPackage->getRequires(), $rootPackage->getDevRequires()), - static function (Link $link) { - $linkTarget = $link->getTarget(); - if (! str_starts_with($linkTarget, 'ext-')) { - return false; - } - - return ExtensionName::isValidExtensionName(substr($linkTarget, strlen('ext-'))); - }, - ); + $extensionsRequired = $this->determineExtensionsRequired->forProject($this->composerFactoryForProject->composer($input, $output)); $pieComposer = PieComposerFactory::createPieComposer( $this->container, @@ -125,7 +114,7 @@ static function (Link $link) { $anyErrorsHappened = false; array_walk( - $rootPackageExtensionsRequired, + $extensionsRequired, function (Link $link) use ($pieComposer, $phpEnabledExtensions, $input, $output, $helper, &$anyErrorsHappened): void { $extension = ExtensionName::normaliseFromString($link->getTarget()); @@ -133,7 +122,7 @@ function (Link $link) use ($pieComposer, $phpEnabledExtensions, $input, $output, $output->writeln(sprintf( '%s: %s ✅ Already installed', $link->getDescription(), - $extension->name(), + $link, )); return; @@ -142,7 +131,7 @@ function (Link $link) use ($pieComposer, $phpEnabledExtensions, $input, $output, $output->writeln(sprintf( '%s: %s ⚠️ Missing', $link->getDescription(), - $extension->name(), + $link, )); try { diff --git a/src/Installing/InstallForPhpProject/ComposerFactoryForProject.php b/src/Installing/InstallForPhpProject/ComposerFactoryForProject.php new file mode 100644 index 00000000..79fd486b --- /dev/null +++ b/src/Installing/InstallForPhpProject/ComposerFactoryForProject.php @@ -0,0 +1,37 @@ +memoizedComposer === null) { + $this->memoizedComposer = ComposerFactory::create(new ConsoleIO( + $input, + $output, + new HelperSet([]), + )); + } + + return $this->memoizedComposer; + } + + public function rootPackage(InputInterface $input, OutputInterface $output): RootPackageInterface + { + return $this->composer($input, $output)->getPackage(); + } +} diff --git a/src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php b/src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php new file mode 100644 index 00000000..7e14a611 --- /dev/null +++ b/src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php @@ -0,0 +1,66 @@ +getTarget(); + if (! str_starts_with($linkTarget, 'ext-')) { + return false; + } + + return ExtensionName::isValidExtensionName(substr($linkTarget, strlen('ext-'))); + } + + /** @return array */ + public function forProject(Composer $composer): array + { + $requires = []; + $removeDevPackages = []; + + /** {@see \Composer\Command\CheckPlatformReqsCommand::execute} */ + $installedRepo = $composer->getRepositoryManager()->getLocalRepository(); + if (! $installedRepo->getPackages()) { + $installedRepo = $composer->getLocker()->getLockedRepository(); + } else { + $removeDevPackages = $installedRepo->getDevPackageNames(); + } + + foreach (array_filter($composer->getPackage()->getDevRequires(), [self::class, 'linkFilter']) as $require => $link) { + $requires[$require] = $link; + } + + $installedRepo = new InstalledRepository([$installedRepo, new RootPackageRepository(clone $composer->getPackage())]); + + foreach ($installedRepo->getPackages() as $package) { + if (in_array($package->getName(), $removeDevPackages, true)) { + continue; + } + + foreach (array_filter($package->getRequires(), [self::class, 'linkFilter']) as $require => $link) { + $requires[$require] = $link; + } + } + + ksort($requires); + + return $requires; + } +} diff --git a/src/Installing/InstallForPhpProject/FindRootPackage.php b/src/Installing/InstallForPhpProject/FindRootPackage.php deleted file mode 100644 index 3e26db98..00000000 --- a/src/Installing/InstallForPhpProject/FindRootPackage.php +++ /dev/null @@ -1,23 +0,0 @@ -getPackage(); - } -} diff --git a/test/integration/Command/InstallExtensionsForProjectCommandTest.php b/test/integration/Command/InstallExtensionsForProjectCommandTest.php index 5376f8ce..009fa2f0 100644 --- a/test/integration/Command/InstallExtensionsForProjectCommandTest.php +++ b/test/integration/Command/InstallExtensionsForProjectCommandTest.php @@ -4,16 +4,20 @@ namespace Php\PieIntegrationTest\Command; +use Composer\Composer; use Composer\Package\Link; use Composer\Package\RootPackage; +use Composer\Repository\InstalledArrayRepository; +use Composer\Repository\RepositoryManager; use Composer\Semver\Constraint\Constraint; use Php\Pie\Command\InstallExtensionsForProjectCommand; use Php\Pie\ComposerIntegration\MinimalHelperSet; use Php\Pie\ComposerIntegration\PieJsonEditor; use Php\Pie\ComposerIntegration\QuieterConsoleIO; use Php\Pie\ExtensionType; +use Php\Pie\Installing\InstallForPhpProject\ComposerFactoryForProject; +use Php\Pie\Installing\InstallForPhpProject\DetermineExtensionsRequired; use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages; -use Php\Pie\Installing\InstallForPhpProject\FindRootPackage; use Php\Pie\Installing\InstallForPhpProject\InstallPiePackageFromPath; use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage; use PHPUnit\Framework\Attributes\CoversClass; @@ -35,7 +39,7 @@ final class InstallExtensionsForProjectCommandTest extends TestCase { private CommandTester $commandTester; - private FindRootPackage&MockObject $findRootpackage; + private ComposerFactoryForProject&MockObject $composerFactoryForProject; private FindMatchingPackages&MockObject $findMatchingPackages; private InstallSelectedPackage&MockObject $installSelectedPackage; private QuestionHelper&MockObject $questionHelper; @@ -63,14 +67,15 @@ function (string $service): mixed { }, ); - $this->findRootpackage = $this->createMock(FindRootPackage::class); - $this->findMatchingPackages = $this->createMock(FindMatchingPackages::class); - $this->installSelectedPackage = $this->createMock(InstallSelectedPackage::class); - $this->installPiePackage = $this->createMock(InstallPiePackageFromPath::class); - $this->questionHelper = $this->createMock(QuestionHelper::class); + $this->composerFactoryForProject = $this->createMock(ComposerFactoryForProject::class); + $this->findMatchingPackages = $this->createMock(FindMatchingPackages::class); + $this->installSelectedPackage = $this->createMock(InstallSelectedPackage::class); + $this->installPiePackage = $this->createMock(InstallPiePackageFromPath::class); + $this->questionHelper = $this->createMock(QuestionHelper::class); $cmd = new InstallExtensionsForProjectCommand( - $this->findRootpackage, + $this->composerFactoryForProject, + new DetermineExtensionsRequired(), $this->findMatchingPackages, $this->installSelectedPackage, $this->installPiePackage, @@ -89,7 +94,18 @@ public function testInstallingExtensionsForPhpProject(): void 'ext-standard' => new Link('my/project', 'ext-standard', new Constraint('=', '*'), Link::TYPE_REQUIRE), 'ext-foobar' => new Link('my/project', 'ext-foobar', new Constraint('=', '*'), Link::TYPE_REQUIRE), ]); - $this->findRootpackage->method('forCwd')->willReturn($rootPackage); + $this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage); + + $installedRepository = new InstalledArrayRepository([$rootPackage]); + + $repositoryManager = $this->createMock(RepositoryManager::class); + $repositoryManager->method('getLocalRepository')->willReturn($installedRepository); + + $composer = $this->createMock(Composer::class); + $composer->method('getPackage')->willReturn($rootPackage); + $composer->method('getRepositoryManager')->willReturn($repositoryManager); + + $this->composerFactoryForProject->method('composer')->willReturn($composer); $this->findMatchingPackages->method('for')->willReturn([ ['name' => 'vendor1/foobar', 'description' => 'The official foobar implementation'], @@ -111,15 +127,15 @@ public function testInstallingExtensionsForPhpProject(): void $this->commandTester->assertCommandIsSuccessful(); self::assertStringContainsString('Checking extensions for your project my/project', $outputString); - self::assertStringContainsString('requires: standard ✅ Already installed', $outputString); - self::assertStringContainsString('requires: foobar ⚠️ Missing', $outputString); + self::assertStringContainsString('requires: my/project requires ext-standard (== *) ✅ Already installed', $outputString); + self::assertStringContainsString('requires: my/project requires ext-foobar (== *) ⚠️ Missing', $outputString); } public function testInstallingExtensionsForPieProject(): void { $rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3'); $rootPackage->setType(ExtensionType::PhpModule->value); - $this->findRootpackage->method('forCwd')->willReturn($rootPackage); + $this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage); $this->installPiePackage ->expects(self::once())