Skip to content

Commit d1766e6

Browse files
authored
Merge pull request #227 from asgrim/check-for-all-exts-required-by-php-project
Check for all exts required by a PHP project
2 parents 397c341 + fa82450 commit d1766e6

File tree

5 files changed

+140
-55
lines changed

5 files changed

+140
-55
lines changed

src/Command/InstallExtensionsForProjectCommand.php

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
use Php\Pie\ComposerIntegration\PieJsonEditor;
1212
use Php\Pie\ExtensionName;
1313
use Php\Pie\ExtensionType;
14+
use Php\Pie\Installing\InstallForPhpProject\ComposerFactoryForProject;
15+
use Php\Pie\Installing\InstallForPhpProject\DetermineExtensionsRequired;
1416
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
15-
use Php\Pie\Installing\InstallForPhpProject\FindRootPackage;
1617
use Php\Pie\Installing\InstallForPhpProject\InstallPiePackageFromPath;
1718
use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage;
1819
use Psr\Container\ContainerInterface;
@@ -26,7 +27,6 @@
2627
use Symfony\Component\Console\Question\ChoiceQuestion;
2728
use Throwable;
2829

29-
use function array_filter;
3030
use function array_keys;
3131
use function array_map;
3232
use function array_merge;
@@ -37,8 +37,6 @@
3737
use function is_string;
3838
use function realpath;
3939
use function sprintf;
40-
use function str_starts_with;
41-
use function strlen;
4240
use function strpos;
4341
use function substr;
4442

@@ -51,7 +49,8 @@
5149
final class InstallExtensionsForProjectCommand extends Command
5250
{
5351
public function __construct(
54-
private readonly FindRootPackage $findRootPackage,
52+
private readonly ComposerFactoryForProject $composerFactoryForProject,
53+
private readonly DetermineExtensionsRequired $determineExtensionsRequired,
5554
private readonly FindMatchingPackages $findMatchingPackages,
5655
private readonly InstallSelectedPackage $installSelectedPackage,
5756
private readonly InstallPiePackageFromPath $installPiePackageFromPath,
@@ -72,7 +71,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
7271
$helper = $this->getHelper('question');
7372
assert($helper instanceof QuestionHelper);
7473

75-
$rootPackage = $this->findRootPackage->forCwd($input, $output);
74+
$rootPackage = $this->composerFactoryForProject->rootPackage($input, $output);
7675

7776
if (ExtensionType::isValid($rootPackage->getType())) {
7877
$cwd = realpath(getcwd());
@@ -100,17 +99,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
10099
getcwd(),
101100
));
102101

103-
$rootPackageExtensionsRequired = array_filter(
104-
array_merge($rootPackage->getRequires(), $rootPackage->getDevRequires()),
105-
static function (Link $link) {
106-
$linkTarget = $link->getTarget();
107-
if (! str_starts_with($linkTarget, 'ext-')) {
108-
return false;
109-
}
110-
111-
return ExtensionName::isValidExtensionName(substr($linkTarget, strlen('ext-')));
112-
},
113-
);
102+
$extensionsRequired = $this->determineExtensionsRequired->forProject($this->composerFactoryForProject->composer($input, $output));
114103

115104
$pieComposer = PieComposerFactory::createPieComposer(
116105
$this->container,
@@ -125,15 +114,15 @@ static function (Link $link) {
125114
$anyErrorsHappened = false;
126115

127116
array_walk(
128-
$rootPackageExtensionsRequired,
117+
$extensionsRequired,
129118
function (Link $link) use ($pieComposer, $phpEnabledExtensions, $input, $output, $helper, &$anyErrorsHappened): void {
130119
$extension = ExtensionName::normaliseFromString($link->getTarget());
131120

132121
if (in_array($extension->name(), $phpEnabledExtensions)) {
133122
$output->writeln(sprintf(
134123
'%s: <info>%s</info> ✅ Already installed',
135124
$link->getDescription(),
136-
$extension->name(),
125+
$link,
137126
));
138127

139128
return;
@@ -142,7 +131,7 @@ function (Link $link) use ($pieComposer, $phpEnabledExtensions, $input, $output,
142131
$output->writeln(sprintf(
143132
'%s: <comment>%s</comment> ⚠️ Missing',
144133
$link->getDescription(),
145-
$extension->name(),
134+
$link,
146135
));
147136

148137
try {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Php\Pie\Installing\InstallForPhpProject;
6+
7+
use Composer\Composer;
8+
use Composer\Factory as ComposerFactory;
9+
use Composer\IO\ConsoleIO;
10+
use Composer\Package\RootPackageInterface;
11+
use Symfony\Component\Console\Helper\HelperSet;
12+
use Symfony\Component\Console\Input\InputInterface;
13+
use Symfony\Component\Console\Output\OutputInterface;
14+
15+
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
16+
class ComposerFactoryForProject
17+
{
18+
private Composer|null $memoizedComposer = null;
19+
20+
public function composer(InputInterface $input, OutputInterface $output): Composer
21+
{
22+
if ($this->memoizedComposer === null) {
23+
$this->memoizedComposer = ComposerFactory::create(new ConsoleIO(
24+
$input,
25+
$output,
26+
new HelperSet([]),
27+
));
28+
}
29+
30+
return $this->memoizedComposer;
31+
}
32+
33+
public function rootPackage(InputInterface $input, OutputInterface $output): RootPackageInterface
34+
{
35+
return $this->composer($input, $output)->getPackage();
36+
}
37+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Php\Pie\Installing\InstallForPhpProject;
6+
7+
use Composer\Composer;
8+
use Composer\Package\Link;
9+
use Composer\Repository\InstalledRepository;
10+
use Composer\Repository\RootPackageRepository;
11+
use Php\Pie\ExtensionName;
12+
13+
use function array_filter;
14+
use function in_array;
15+
use function ksort;
16+
use function str_starts_with;
17+
use function strlen;
18+
use function substr;
19+
20+
class DetermineExtensionsRequired
21+
{
22+
public static function linkFilter(Link $link): bool
23+
{
24+
$linkTarget = $link->getTarget();
25+
if (! str_starts_with($linkTarget, 'ext-')) {
26+
return false;
27+
}
28+
29+
return ExtensionName::isValidExtensionName(substr($linkTarget, strlen('ext-')));
30+
}
31+
32+
/** @return array<string, Link> */
33+
public function forProject(Composer $composer): array
34+
{
35+
$requires = [];
36+
$removeDevPackages = [];
37+
38+
/** {@see \Composer\Command\CheckPlatformReqsCommand::execute} */
39+
$installedRepo = $composer->getRepositoryManager()->getLocalRepository();
40+
if (! $installedRepo->getPackages()) {
41+
$installedRepo = $composer->getLocker()->getLockedRepository();
42+
} else {
43+
$removeDevPackages = $installedRepo->getDevPackageNames();
44+
}
45+
46+
foreach (array_filter($composer->getPackage()->getDevRequires(), [self::class, 'linkFilter']) as $require => $link) {
47+
$requires[$require] = $link;
48+
}
49+
50+
$installedRepo = new InstalledRepository([$installedRepo, new RootPackageRepository(clone $composer->getPackage())]);
51+
52+
foreach ($installedRepo->getPackages() as $package) {
53+
if (in_array($package->getName(), $removeDevPackages, true)) {
54+
continue;
55+
}
56+
57+
foreach (array_filter($package->getRequires(), [self::class, 'linkFilter']) as $require => $link) {
58+
$requires[$require] = $link;
59+
}
60+
}
61+
62+
ksort($requires);
63+
64+
return $requires;
65+
}
66+
}

src/Installing/InstallForPhpProject/FindRootPackage.php

Lines changed: 0 additions & 23 deletions
This file was deleted.

test/integration/Command/InstallExtensionsForProjectCommandTest.php

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@
44

55
namespace Php\PieIntegrationTest\Command;
66

7+
use Composer\Composer;
78
use Composer\Package\Link;
89
use Composer\Package\RootPackage;
10+
use Composer\Repository\InstalledArrayRepository;
11+
use Composer\Repository\RepositoryManager;
912
use Composer\Semver\Constraint\Constraint;
1013
use Php\Pie\Command\InstallExtensionsForProjectCommand;
1114
use Php\Pie\ComposerIntegration\MinimalHelperSet;
1215
use Php\Pie\ComposerIntegration\PieJsonEditor;
1316
use Php\Pie\ComposerIntegration\QuieterConsoleIO;
1417
use Php\Pie\ExtensionType;
18+
use Php\Pie\Installing\InstallForPhpProject\ComposerFactoryForProject;
19+
use Php\Pie\Installing\InstallForPhpProject\DetermineExtensionsRequired;
1520
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
16-
use Php\Pie\Installing\InstallForPhpProject\FindRootPackage;
1721
use Php\Pie\Installing\InstallForPhpProject\InstallPiePackageFromPath;
1822
use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage;
1923
use PHPUnit\Framework\Attributes\CoversClass;
@@ -35,7 +39,7 @@
3539
final class InstallExtensionsForProjectCommandTest extends TestCase
3640
{
3741
private CommandTester $commandTester;
38-
private FindRootPackage&MockObject $findRootpackage;
42+
private ComposerFactoryForProject&MockObject $composerFactoryForProject;
3943
private FindMatchingPackages&MockObject $findMatchingPackages;
4044
private InstallSelectedPackage&MockObject $installSelectedPackage;
4145
private QuestionHelper&MockObject $questionHelper;
@@ -63,14 +67,15 @@ function (string $service): mixed {
6367
},
6468
);
6569

66-
$this->findRootpackage = $this->createMock(FindRootPackage::class);
67-
$this->findMatchingPackages = $this->createMock(FindMatchingPackages::class);
68-
$this->installSelectedPackage = $this->createMock(InstallSelectedPackage::class);
69-
$this->installPiePackage = $this->createMock(InstallPiePackageFromPath::class);
70-
$this->questionHelper = $this->createMock(QuestionHelper::class);
70+
$this->composerFactoryForProject = $this->createMock(ComposerFactoryForProject::class);
71+
$this->findMatchingPackages = $this->createMock(FindMatchingPackages::class);
72+
$this->installSelectedPackage = $this->createMock(InstallSelectedPackage::class);
73+
$this->installPiePackage = $this->createMock(InstallPiePackageFromPath::class);
74+
$this->questionHelper = $this->createMock(QuestionHelper::class);
7175

7276
$cmd = new InstallExtensionsForProjectCommand(
73-
$this->findRootpackage,
77+
$this->composerFactoryForProject,
78+
new DetermineExtensionsRequired(),
7479
$this->findMatchingPackages,
7580
$this->installSelectedPackage,
7681
$this->installPiePackage,
@@ -89,7 +94,18 @@ public function testInstallingExtensionsForPhpProject(): void
8994
'ext-standard' => new Link('my/project', 'ext-standard', new Constraint('=', '*'), Link::TYPE_REQUIRE),
9095
'ext-foobar' => new Link('my/project', 'ext-foobar', new Constraint('=', '*'), Link::TYPE_REQUIRE),
9196
]);
92-
$this->findRootpackage->method('forCwd')->willReturn($rootPackage);
97+
$this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage);
98+
99+
$installedRepository = new InstalledArrayRepository([$rootPackage]);
100+
101+
$repositoryManager = $this->createMock(RepositoryManager::class);
102+
$repositoryManager->method('getLocalRepository')->willReturn($installedRepository);
103+
104+
$composer = $this->createMock(Composer::class);
105+
$composer->method('getPackage')->willReturn($rootPackage);
106+
$composer->method('getRepositoryManager')->willReturn($repositoryManager);
107+
108+
$this->composerFactoryForProject->method('composer')->willReturn($composer);
93109

94110
$this->findMatchingPackages->method('for')->willReturn([
95111
['name' => 'vendor1/foobar', 'description' => 'The official foobar implementation'],
@@ -111,15 +127,15 @@ public function testInstallingExtensionsForPhpProject(): void
111127

112128
$this->commandTester->assertCommandIsSuccessful();
113129
self::assertStringContainsString('Checking extensions for your project my/project', $outputString);
114-
self::assertStringContainsString('requires: standard ✅ Already installed', $outputString);
115-
self::assertStringContainsString('requires: foobar ⚠️ Missing', $outputString);
130+
self::assertStringContainsString('requires: my/project requires ext-standard (== *) ✅ Already installed', $outputString);
131+
self::assertStringContainsString('requires: my/project requires ext-foobar (== *) ⚠️ Missing', $outputString);
116132
}
117133

118134
public function testInstallingExtensionsForPieProject(): void
119135
{
120136
$rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3');
121137
$rootPackage->setType(ExtensionType::PhpModule->value);
122-
$this->findRootpackage->method('forCwd')->willReturn($rootPackage);
138+
$this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage);
123139

124140
$this->installPiePackage
125141
->expects(self::once())

0 commit comments

Comments
 (0)