From 81ad5b289c16e2935cfac9c4939f8911f8b6fc05 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 21 Apr 2025 07:54:47 +0100 Subject: [PATCH 1/3] Rename component to be a more general composer factory --- .../InstallExtensionsForProjectCommand.php | 6 +-- .../ComposerFactoryForProject.php | 37 +++++++++++++++++++ .../InstallForPhpProject/FindRootPackage.php | 23 ------------ ...InstallExtensionsForProjectCommandTest.php | 20 +++++----- 4 files changed, 50 insertions(+), 36 deletions(-) create mode 100644 src/Installing/InstallForPhpProject/ComposerFactoryForProject.php delete mode 100644 src/Installing/InstallForPhpProject/FindRootPackage.php diff --git a/src/Command/InstallExtensionsForProjectCommand.php b/src/Command/InstallExtensionsForProjectCommand.php index cf87564b..ac8ea3f1 100644 --- a/src/Command/InstallExtensionsForProjectCommand.php +++ b/src/Command/InstallExtensionsForProjectCommand.php @@ -11,8 +11,8 @@ use Php\Pie\ComposerIntegration\PieJsonEditor; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\Installing\InstallForPhpProject\ComposerFactoryForProject; 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; @@ -51,7 +51,7 @@ final class InstallExtensionsForProjectCommand extends Command { public function __construct( - private readonly FindRootPackage $findRootPackage, + private readonly ComposerFactoryForProject $composerFactoryForProject, private readonly FindMatchingPackages $findMatchingPackages, private readonly InstallSelectedPackage $installSelectedPackage, private readonly InstallPiePackageFromPath $installPiePackageFromPath, @@ -72,7 +72,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()); 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/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..ee5c64ab 100644 --- a/test/integration/Command/InstallExtensionsForProjectCommandTest.php +++ b/test/integration/Command/InstallExtensionsForProjectCommandTest.php @@ -12,8 +12,8 @@ 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\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 +35,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 +63,14 @@ 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, $this->findMatchingPackages, $this->installSelectedPackage, $this->installPiePackage, @@ -89,7 +89,7 @@ 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); $this->findMatchingPackages->method('for')->willReturn([ ['name' => 'vendor1/foobar', 'description' => 'The official foobar implementation'], @@ -119,7 +119,7 @@ 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()) From a34b3822d9d02c0cd66b55b9c17d7fc25d2aa1e2 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 21 Apr 2025 08:12:57 +0100 Subject: [PATCH 2/3] Split component for determining which extensions are needed for a project --- .../InstallExtensionsForProjectCommand.php | 19 +++-------- .../DetermineExtensionsRequired.php | 34 +++++++++++++++++++ ...InstallExtensionsForProjectCommandTest.php | 2 ++ 3 files changed, 40 insertions(+), 15 deletions(-) create mode 100644 src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php diff --git a/src/Command/InstallExtensionsForProjectCommand.php b/src/Command/InstallExtensionsForProjectCommand.php index ac8ea3f1..c1fe10a3 100644 --- a/src/Command/InstallExtensionsForProjectCommand.php +++ b/src/Command/InstallExtensionsForProjectCommand.php @@ -12,6 +12,7 @@ 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\InstallPiePackageFromPath; use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage; @@ -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; @@ -52,6 +50,7 @@ final class InstallExtensionsForProjectCommand extends Command { public function __construct( private readonly ComposerFactoryForProject $composerFactoryForProject, + private readonly DetermineExtensionsRequired $determineExtensionsRequired, private readonly FindMatchingPackages $findMatchingPackages, private readonly InstallSelectedPackage $installSelectedPackage, private readonly InstallPiePackageFromPath $installPiePackageFromPath, @@ -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($rootPackage); $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()); diff --git a/src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php b/src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php new file mode 100644 index 00000000..6d1fd420 --- /dev/null +++ b/src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php @@ -0,0 +1,34 @@ + */ + public function forProject(RootPackageInterface $rootPackage): array + { + return 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-'))); + }, + ); + } +} diff --git a/test/integration/Command/InstallExtensionsForProjectCommandTest.php b/test/integration/Command/InstallExtensionsForProjectCommandTest.php index ee5c64ab..4c9944e8 100644 --- a/test/integration/Command/InstallExtensionsForProjectCommandTest.php +++ b/test/integration/Command/InstallExtensionsForProjectCommandTest.php @@ -13,6 +13,7 @@ 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\InstallPiePackageFromPath; use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage; @@ -71,6 +72,7 @@ function (string $service): mixed { $cmd = new InstallExtensionsForProjectCommand( $this->composerFactoryForProject, + new DetermineExtensionsRequired(), $this->findMatchingPackages, $this->installSelectedPackage, $this->installPiePackage, From fa82450359875345944f18682631e7857eeb544c Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 29 Apr 2025 09:26:03 +0100 Subject: [PATCH 3/3] Determine extensions required for a PHP project using local or locked repo --- .../InstallExtensionsForProjectCommand.php | 6 +- .../DetermineExtensionsRequired.php | 60 ++++++++++++++----- ...InstallExtensionsForProjectCommandTest.php | 18 +++++- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/src/Command/InstallExtensionsForProjectCommand.php b/src/Command/InstallExtensionsForProjectCommand.php index c1fe10a3..e80f0083 100644 --- a/src/Command/InstallExtensionsForProjectCommand.php +++ b/src/Command/InstallExtensionsForProjectCommand.php @@ -99,7 +99,7 @@ public function execute(InputInterface $input, OutputInterface $output): int getcwd(), )); - $extensionsRequired = $this->determineExtensionsRequired->forProject($rootPackage); + $extensionsRequired = $this->determineExtensionsRequired->forProject($this->composerFactoryForProject->composer($input, $output)); $pieComposer = PieComposerFactory::createPieComposer( $this->container, @@ -122,7 +122,7 @@ function (Link $link) use ($pieComposer, $phpEnabledExtensions, $input, $output, $output->writeln(sprintf( '%s: %s ✅ Already installed', $link->getDescription(), - $extension->name(), + $link, )); return; @@ -131,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/DetermineExtensionsRequired.php b/src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php index 6d1fd420..7e14a611 100644 --- a/src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php +++ b/src/Installing/InstallForPhpProject/DetermineExtensionsRequired.php @@ -4,31 +4,63 @@ namespace Php\Pie\Installing\InstallForPhpProject; +use Composer\Composer; use Composer\Package\Link; -use Composer\Package\RootPackageInterface; +use Composer\Repository\InstalledRepository; +use Composer\Repository\RootPackageRepository; use Php\Pie\ExtensionName; use function array_filter; -use function array_merge; +use function in_array; +use function ksort; use function str_starts_with; use function strlen; use function substr; class DetermineExtensionsRequired { + public static function linkFilter(Link $link): bool + { + $linkTarget = $link->getTarget(); + if (! str_starts_with($linkTarget, 'ext-')) { + return false; + } + + return ExtensionName::isValidExtensionName(substr($linkTarget, strlen('ext-'))); + } + /** @return array */ - public function forProject(RootPackageInterface $rootPackage): array + public function forProject(Composer $composer): array { - return 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-'))); - }, - ); + $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/test/integration/Command/InstallExtensionsForProjectCommandTest.php b/test/integration/Command/InstallExtensionsForProjectCommandTest.php index 4c9944e8..009fa2f0 100644 --- a/test/integration/Command/InstallExtensionsForProjectCommandTest.php +++ b/test/integration/Command/InstallExtensionsForProjectCommandTest.php @@ -4,8 +4,11 @@ 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; @@ -93,6 +96,17 @@ public function testInstallingExtensionsForPhpProject(): void ]); $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'], ['name' => 'vendor2/afoobar', 'description' => 'An improved async foobar extension'], @@ -113,8 +127,8 @@ 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