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())