Skip to content

Commit 76f148c

Browse files
authored
Merge pull request #213 from asgrim/check-exts-in-project
Add "pie install" to install missing extensions for a PHP project
2 parents bd03d4a + 3b92107 commit 76f148c

File tree

14 files changed

+612
-9
lines changed

14 files changed

+612
-9
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,36 @@ You must now add "extension=example_pie_extension" to your php.ini
4949
$
5050
```
5151

52+
## Installing all extensions for a project
53+
54+
When in your PHP project, you can install any missing top-level extensions:
55+
56+
```
57+
$ pie install
58+
🥧 PHP Installer for Extensions (PIE), 0.9.0, from The PHP Foundation
59+
You are running PHP 8.3.19
60+
Target PHP installation: 8.3.19 nts, on Linux/OSX/etc x86_64 (from /usr/bin/php8.3)
61+
Checking extensions for your project your-vendor/your-project
62+
requires: curl ✅ Already installed
63+
requires: intl ✅ Already installed
64+
requires: json ✅ Already installed
65+
requires: example_pie_extension ⚠️ Missing
66+
67+
The following packages may be suitable, which would you like to install:
68+
[0] None
69+
[1] asgrim/example-pie-extension: Example PIE extension
70+
> 1
71+
> 🥧 PHP Installer for Extensions (PIE), 0.9.0, from The PHP Foundation
72+
> This command may need elevated privileges, and may prompt you for your password.
73+
> You are running PHP 8.3.19
74+
> Target PHP installation: 8.3.19 nts, on Linux/OSX/etc x86_64 (from /usr/bin/php8.3)
75+
> Found package: asgrim/example-pie-extension:2.0.2 which provides ext-example_pie_extension
76+
... (snip) ...
77+
> ✅ Extension is enabled and loaded in /usr/bin/php8.3
78+
79+
Finished checking extensions.
80+
```
81+
5282
## More documentation...
5383

5484
The full documentation for PIE can be found in [usage](./docs/usage.md) docs.

bin/pie

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ declare(strict_types=1);
66
namespace Php\Pie;
77

88
use Php\Pie\Command\BuildCommand;
9+
use Php\Pie\Command\InstallExtensionsForProjectCommand;
910
use Php\Pie\Command\DownloadCommand;
1011
use Php\Pie\Command\InfoCommand;
1112
use Php\Pie\Command\InstallCommand;
@@ -44,6 +45,7 @@ $application->setCommandLoader(new ContainerCommandLoader(
4445
'repository:remove' => RepositoryRemoveCommand::class,
4546
'uninstall' => UninstallCommand::class,
4647
'self-update' => SelfUpdateCommand::class,
48+
'install-extensions-for-project' => InstallExtensionsForProjectCommand::class,
4749
]
4850
));
4951

docs/usage.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,39 @@ You can list the repositories for the target PHP installation with:
258258

259259
* `pie repository:list [--with-php-config=...]`
260260

261+
## Check and install missing extensions for your project
262+
263+
You can use `pie install` when in a PHP project working directory to check the
264+
extensions the project requires are present. If an extension is missing, PIE
265+
will try to find an installation candidate and interactively ask if you would
266+
like to install one. For example:
267+
268+
```
269+
$ pie install
270+
🥧 PHP Installer for Extensions (PIE), 0.9.0, from The PHP Foundation
271+
You are running PHP 8.3.19
272+
Target PHP installation: 8.3.19 nts, on Linux/OSX/etc x86_64 (from /usr/bin/php8.3)
273+
Checking extensions for your project your-vendor/your-project
274+
requires: curl ✅ Already installed
275+
requires: intl ✅ Already installed
276+
requires: json ✅ Already installed
277+
requires: example_pie_extension ⚠️ Missing
278+
279+
The following packages may be suitable, which would you like to install:
280+
[0] None
281+
[1] asgrim/example-pie-extension: Example PIE extension
282+
> 1
283+
> 🥧 PHP Installer for Extensions (PIE), 0.9.0, from The PHP Foundation
284+
> This command may need elevated privileges, and may prompt you for your password.
285+
> You are running PHP 8.3.19
286+
> Target PHP installation: 8.3.19 nts, on Linux/OSX/etc x86_64 (from /usr/bin/php8.3)
287+
> Found package: asgrim/example-pie-extension:2.0.2 which provides ext-example_pie_extension
288+
... (snip) ...
289+
> ✅ Extension is enabled and loaded in /usr/bin/php8.3
290+
291+
Finished checking extensions.
292+
```
293+
261294
## Comparison with PECL
262295

263296
Since PIE is a replacement for PECL, here is a comparison of the commands that

src/Command/CommandHelper.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@
3838
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
3939
final class CommandHelper
4040
{
41-
private const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version';
42-
private const OPTION_WITH_PHP_CONFIG = 'with-php-config';
43-
private const OPTION_WITH_PHP_PATH = 'with-php-path';
44-
private const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path';
45-
private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs';
46-
private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension';
47-
private const OPTION_FORCE = 'force';
41+
public const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version';
42+
public const OPTION_WITH_PHP_CONFIG = 'with-php-config';
43+
public const OPTION_WITH_PHP_PATH = 'with-php-path';
44+
public const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path';
45+
private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs';
46+
private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension';
47+
private const OPTION_FORCE = 'force';
4848

4949
/** @psalm-suppress UnusedConstructor */
5050
private function __construct()
@@ -77,7 +77,7 @@ public static function configureDownloadBuildInstallOptions(Command $command): v
7777
{
7878
$command->addArgument(
7979
self::ARG_REQUESTED_PACKAGE_AND_VERSION,
80-
InputArgument::REQUIRED,
80+
InputArgument::OPTIONAL,
8181
'The PIE package name and version constraint to use, in the format {vendor/package}{?:{?version-constraint}{?@stability}}, for example `xdebug/xdebug:^3.4@alpha`, `xdebug/xdebug:@alpha`, `xdebug/xdebug:^3.4`, etc.',
8282
);
8383
$command->addOption(

src/Command/InstallCommand.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,17 @@
1414
use Psr\Container\ContainerInterface;
1515
use Symfony\Component\Console\Attribute\AsCommand;
1616
use Symfony\Component\Console\Command\Command;
17+
use Symfony\Component\Console\Input\ArrayInput;
1718
use Symfony\Component\Console\Input\InputInterface;
1819
use Symfony\Component\Console\Output\OutputInterface;
19-
20+
use Webmozart\Assert\Assert;
21+
22+
use function array_combine;
23+
use function array_filter;
24+
use function array_keys;
25+
use function array_map;
26+
use function array_merge;
27+
use function array_values;
2028
use function sprintf;
2129

2230
#[AsCommand(
@@ -40,8 +48,29 @@ public function configure(): void
4048
CommandHelper::configureDownloadBuildInstallOptions($this);
4149
}
4250

51+
private function invokeInstallForProject(InputInterface $input, OutputInterface $output): int
52+
{
53+
$originalSuppliedOptions = array_filter($input->getOptions());
54+
$installForProjectInput = new ArrayInput(array_merge(
55+
['command' => 'install-extensions-for-project'],
56+
array_combine(
57+
array_map(static fn ($key) => '--' . $key, array_keys($originalSuppliedOptions)),
58+
array_values($originalSuppliedOptions),
59+
),
60+
));
61+
62+
$application = $this->getApplication();
63+
Assert::notNull($application);
64+
65+
return $application->doRun($installForProjectInput, $output);
66+
}
67+
4368
public function execute(InputInterface $input, OutputInterface $output): int
4469
{
70+
if (! $input->getArgument(CommandHelper::ARG_REQUESTED_PACKAGE_AND_VERSION)) {
71+
return $this->invokeInstallForProject($input, $output);
72+
}
73+
4574
if (! TargetPlatform::isRunningAsRoot()) {
4675
$output->writeln('This command may need elevated privileges, and may prompt you for your password.');
4776
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Php\Pie\Command;
6+
7+
use Composer\Package\Link;
8+
use OutOfRangeException;
9+
use Php\Pie\ComposerIntegration\PieComposerFactory;
10+
use Php\Pie\ComposerIntegration\PieComposerRequest;
11+
use Php\Pie\ExtensionName;
12+
use Php\Pie\ExtensionType;
13+
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
14+
use Php\Pie\Installing\InstallForPhpProject\FindRootPackage;
15+
use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage;
16+
use Psr\Container\ContainerInterface;
17+
use Symfony\Component\Console\Attribute\AsCommand;
18+
use Symfony\Component\Console\Command\Command;
19+
use Symfony\Component\Console\Helper\QuestionHelper;
20+
use Symfony\Component\Console\Input\InputInterface;
21+
use Symfony\Component\Console\Output\ConsoleOutputInterface;
22+
use Symfony\Component\Console\Output\NullOutput;
23+
use Symfony\Component\Console\Output\OutputInterface;
24+
use Symfony\Component\Console\Question\ChoiceQuestion;
25+
use Throwable;
26+
27+
use function array_filter;
28+
use function array_keys;
29+
use function array_map;
30+
use function array_merge;
31+
use function array_walk;
32+
use function assert;
33+
use function getcwd;
34+
use function in_array;
35+
use function sprintf;
36+
use function str_starts_with;
37+
use function strlen;
38+
use function strpos;
39+
use function substr;
40+
41+
use const PHP_EOL;
42+
43+
#[AsCommand(
44+
name: 'install-extensions-for-project',
45+
description: 'Check a project for its extension dependencies, and offers to install them',
46+
)]
47+
final class InstallExtensionsForProjectCommand extends Command
48+
{
49+
public function __construct(
50+
private readonly FindRootPackage $findRootPackage,
51+
private readonly FindMatchingPackages $findMatchingPackages,
52+
private readonly InstallSelectedPackage $installSelectedPackage,
53+
private readonly ContainerInterface $container,
54+
) {
55+
parent::__construct();
56+
}
57+
58+
public function configure(): void
59+
{
60+
parent::configure();
61+
62+
CommandHelper::configurePhpConfigOptions($this);
63+
}
64+
65+
public function execute(InputInterface $input, OutputInterface $output): int
66+
{
67+
$helper = $this->getHelper('question');
68+
assert($helper instanceof QuestionHelper);
69+
70+
$targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
71+
72+
$rootPackage = $this->findRootPackage->forCwd($input, $output);
73+
74+
if (ExtensionType::isValid($rootPackage->getType())) {
75+
$output->writeln('<error>This composer.json is for an extension, installing missing packages is not supported.</error>');
76+
77+
return Command::INVALID;
78+
}
79+
80+
$output->writeln(sprintf(
81+
'Checking extensions for your project <info>%s</info> (path: %s)',
82+
$rootPackage->getPrettyName(),
83+
getcwd(),
84+
));
85+
86+
$rootPackageExtensionsRequired = array_filter(
87+
array_merge($rootPackage->getRequires(), $rootPackage->getDevRequires()),
88+
static function (Link $link) {
89+
$linkTarget = $link->getTarget();
90+
if (! str_starts_with($linkTarget, 'ext-')) {
91+
return false;
92+
}
93+
94+
return ExtensionName::isValidExtensionName(substr($linkTarget, strlen('ext-')));
95+
},
96+
);
97+
98+
$pieComposer = PieComposerFactory::createPieComposer(
99+
$this->container,
100+
PieComposerRequest::noOperation(
101+
new NullOutput(),
102+
$targetPlatform,
103+
),
104+
);
105+
106+
$phpEnabledExtensions = array_keys($targetPlatform->phpBinaryPath->extensions());
107+
108+
$anyErrorsHappened = false;
109+
110+
array_walk(
111+
$rootPackageExtensionsRequired,
112+
function (Link $link) use ($pieComposer, $phpEnabledExtensions, $input, $output, $helper, &$anyErrorsHappened): void {
113+
$extension = ExtensionName::normaliseFromString($link->getTarget());
114+
115+
if (in_array($extension->name(), $phpEnabledExtensions)) {
116+
$output->writeln(sprintf(
117+
'%s: <info>%s</info> ✅ Already installed',
118+
$link->getDescription(),
119+
$extension->name(),
120+
));
121+
122+
return;
123+
}
124+
125+
$output->writeln(sprintf(
126+
'%s: <comment>%s</comment> ⚠️ Missing',
127+
$link->getDescription(),
128+
$extension->name(),
129+
));
130+
131+
try {
132+
$matches = $this->findMatchingPackages->for($pieComposer, $extension);
133+
} catch (OutOfRangeException) {
134+
$anyErrorsHappened = true;
135+
136+
$message = sprintf(
137+
'<error>No packages were found for %s</error>',
138+
$extension->nameWithExtPrefix(),
139+
);
140+
141+
if ($output instanceof ConsoleOutputInterface) {
142+
$output->getErrorOutput()->writeln($message);
143+
144+
return;
145+
}
146+
147+
$output->writeln($message);
148+
149+
return;
150+
}
151+
152+
$choiceQuestion = new ChoiceQuestion(
153+
"\nThe following packages may be suitable, which would you like to install: ",
154+
array_merge(
155+
['None'],
156+
array_map(
157+
static function (array $match): string {
158+
return sprintf('%s: %s', $match['name'], $match['description'] ?? 'no description available');
159+
},
160+
$matches,
161+
),
162+
),
163+
0,
164+
);
165+
166+
$selectedPackageAnswer = (string) $helper->ask($input, $output, $choiceQuestion);
167+
168+
if ($selectedPackageAnswer === 'None') {
169+
$output->writeln('Okay I won\'t install anything for ' . $extension->name());
170+
171+
return;
172+
}
173+
174+
try {
175+
$this->installSelectedPackage->withPieCli(
176+
substr($selectedPackageAnswer, 0, (int) strpos($selectedPackageAnswer, ':')),
177+
$input,
178+
$output,
179+
);
180+
} catch (Throwable $t) {
181+
$anyErrorsHappened = true;
182+
183+
$message = '<error>' . $t->getMessage() . '</error>';
184+
185+
if ($output instanceof ConsoleOutputInterface) {
186+
$output->getErrorOutput()->writeln($message);
187+
188+
return;
189+
}
190+
191+
$output->writeln($message);
192+
}
193+
},
194+
);
195+
196+
$output->writeln(PHP_EOL . 'Finished checking extensions.');
197+
198+
/**
199+
* @psalm-suppress TypeDoesNotContainType
200+
* @psalm-suppress RedundantCondition
201+
*/
202+
return $anyErrorsHappened ? self::FAILURE : self::SUCCESS;
203+
}
204+
}

src/Container.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Php\Pie\Command\DownloadCommand;
1414
use Php\Pie\Command\InfoCommand;
1515
use Php\Pie\Command\InstallCommand;
16+
use Php\Pie\Command\InstallExtensionsForProjectCommand;
1617
use Php\Pie\Command\RepositoryAddCommand;
1718
use Php\Pie\Command\RepositoryListCommand;
1819
use Php\Pie\Command\RepositoryRemoveCommand;
@@ -58,6 +59,7 @@ public static function factory(): ContainerInterface
5859
$container->singleton(RepositoryRemoveCommand::class);
5960
$container->singleton(UninstallCommand::class);
6061
$container->singleton(SelfUpdateCommand::class);
62+
$container->singleton(InstallExtensionsForProjectCommand::class);
6163

6264
$container->singleton(QuieterConsoleIO::class, static function (ContainerInterface $container): QuieterConsoleIO {
6365
return new QuieterConsoleIO(

0 commit comments

Comments
 (0)