Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 9 additions & 20 deletions src/Command/InstallExtensionsForProjectCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -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,
Expand All @@ -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());
Expand Down Expand Up @@ -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,
Expand All @@ -125,15 +114,15 @@ 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());

if (in_array($extension->name(), $phpEnabledExtensions)) {
$output->writeln(sprintf(
'%s: <info>%s</info> ✅ Already installed',
$link->getDescription(),
$extension->name(),
$link,
));

return;
Expand All @@ -142,7 +131,7 @@ function (Link $link) use ($pieComposer, $phpEnabledExtensions, $input, $output,
$output->writeln(sprintf(
'%s: <comment>%s</comment> ⚠️ Missing',
$link->getDescription(),
$extension->name(),
$link,
));

try {
Expand Down
37 changes: 37 additions & 0 deletions src/Installing/InstallForPhpProject/ComposerFactoryForProject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Php\Pie\Installing\InstallForPhpProject;

use Composer\Composer;
use Composer\Factory as ComposerFactory;
use Composer\IO\ConsoleIO;
use Composer\Package\RootPackageInterface;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
class ComposerFactoryForProject
{
private Composer|null $memoizedComposer = null;

public function composer(InputInterface $input, OutputInterface $output): Composer
{
if ($this->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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Php\Pie\Installing\InstallForPhpProject;

use Composer\Composer;
use Composer\Package\Link;
use Composer\Repository\InstalledRepository;
use Composer\Repository\RootPackageRepository;
use Php\Pie\ExtensionName;

use function array_filter;
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<string, Link> */
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;
}
}
23 changes: 0 additions & 23 deletions src/Installing/InstallForPhpProject/FindRootPackage.php

This file was deleted.

40 changes: 28 additions & 12 deletions test/integration/Command/InstallExtensionsForProjectCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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'],
Expand All @@ -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())
Expand Down