diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php
index 4f1237b4..fd2c8fde 100644
--- a/src/Command/InstallCommand.php
+++ b/src/Command/InstallCommand.php
@@ -14,17 +14,9 @@
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-use Webmozart\Assert\Assert;
-
-use function array_combine;
-use function array_filter;
-use function array_keys;
-use function array_map;
-use function array_merge;
-use function array_values;
+
use function sprintf;
#[AsCommand(
@@ -37,6 +29,7 @@ public function __construct(
private readonly ContainerInterface $container,
private readonly DependencyResolver $dependencyResolver,
private readonly ComposerIntegrationHandler $composerIntegrationHandler,
+ private readonly InvokeSubCommand $invokeSubCommand,
) {
parent::__construct();
}
@@ -48,27 +41,15 @@ public function configure(): void
CommandHelper::configureDownloadBuildInstallOptions($this);
}
- private function invokeInstallForProject(InputInterface $input, OutputInterface $output): int
- {
- $originalSuppliedOptions = array_filter($input->getOptions());
- $installForProjectInput = new ArrayInput(array_merge(
- ['command' => 'install-extensions-for-project'],
- array_combine(
- array_map(static fn ($key) => '--' . $key, array_keys($originalSuppliedOptions)),
- array_values($originalSuppliedOptions),
- ),
- ));
-
- $application = $this->getApplication();
- Assert::notNull($application);
-
- return $application->doRun($installForProjectInput, $output);
- }
-
public function execute(InputInterface $input, OutputInterface $output): int
{
if (! $input->getArgument(CommandHelper::ARG_REQUESTED_PACKAGE_AND_VERSION)) {
- return $this->invokeInstallForProject($input, $output);
+ return ($this->invokeSubCommand)(
+ $this,
+ ['command' => 'install-extensions-for-project'],
+ $input,
+ $output,
+ );
}
if (! TargetPlatform::isRunningAsRoot()) {
diff --git a/src/Command/InstallExtensionsForProjectCommand.php b/src/Command/InstallExtensionsForProjectCommand.php
index b96f9366..cf87564b 100644
--- a/src/Command/InstallExtensionsForProjectCommand.php
+++ b/src/Command/InstallExtensionsForProjectCommand.php
@@ -8,10 +8,12 @@
use OutOfRangeException;
use Php\Pie\ComposerIntegration\PieComposerFactory;
use Php\Pie\ComposerIntegration\PieComposerRequest;
+use Php\Pie\ComposerIntegration\PieJsonEditor;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
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;
use Symfony\Component\Console\Attribute\AsCommand;
@@ -32,6 +34,8 @@
use function assert;
use function getcwd;
use function in_array;
+use function is_string;
+use function realpath;
use function sprintf;
use function str_starts_with;
use function strlen;
@@ -50,6 +54,7 @@ public function __construct(
private readonly FindRootPackage $findRootPackage,
private readonly FindMatchingPackages $findMatchingPackages,
private readonly InstallSelectedPackage $installSelectedPackage,
+ private readonly InstallPiePackageFromPath $installPiePackageFromPath,
private readonly ContainerInterface $container,
) {
parent::__construct();
@@ -67,16 +72,28 @@ public function execute(InputInterface $input, OutputInterface $output): int
$helper = $this->getHelper('question');
assert($helper instanceof QuestionHelper);
- $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
-
$rootPackage = $this->findRootPackage->forCwd($input, $output);
if (ExtensionType::isValid($rootPackage->getType())) {
- $output->writeln('This composer.json is for an extension, installing missing packages is not supported.');
-
- return Command::INVALID;
+ $cwd = realpath(getcwd());
+ if (! is_string($cwd) || $cwd === '') {
+ $output->writeln('Failed to determine current working directory.');
+
+ return Command::FAILURE;
+ }
+
+ return ($this->installPiePackageFromPath)(
+ $this,
+ $cwd,
+ $rootPackage,
+ PieJsonEditor::fromTargetPlatform(CommandHelper::determineTargetPlatformFromInputs($input, new NullOutput())),
+ $input,
+ $output,
+ );
}
+ $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
+
$output->writeln(sprintf(
'Checking extensions for your project %s (path: %s)',
$rootPackage->getPrettyName(),
diff --git a/src/Command/InvokeSubCommand.php b/src/Command/InvokeSubCommand.php
new file mode 100644
index 00000000..b6b2546a
--- /dev/null
+++ b/src/Command/InvokeSubCommand.php
@@ -0,0 +1,44 @@
+ $subCommandInput */
+ public function __invoke(
+ Command $command,
+ array $subCommandInput,
+ InputInterface $originalCommandInput,
+ OutputInterface $output,
+ ): int {
+ $originalSuppliedOptions = array_filter($originalCommandInput->getOptions());
+ $installForProjectInput = new ArrayInput(array_merge(
+ $subCommandInput,
+ array_combine(
+ array_map(static fn ($key) => '--' . $key, array_keys($originalSuppliedOptions)),
+ array_values($originalSuppliedOptions),
+ ),
+ ));
+
+ $application = $command->getApplication();
+ Assert::notNull($application);
+
+ return $application->doRun($installForProjectInput, $output);
+ }
+}
diff --git a/src/Installing/InstallForPhpProject/FindRootPackage.php b/src/Installing/InstallForPhpProject/FindRootPackage.php
index 3f6ab111..3e26db98 100644
--- a/src/Installing/InstallForPhpProject/FindRootPackage.php
+++ b/src/Installing/InstallForPhpProject/FindRootPackage.php
@@ -11,6 +11,7 @@
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 FindRootPackage
{
public function forCwd(InputInterface $input, OutputInterface $output): RootPackageInterface
diff --git a/src/Installing/InstallForPhpProject/InstallPiePackageFromPath.php b/src/Installing/InstallForPhpProject/InstallPiePackageFromPath.php
new file mode 100644
index 00000000..73ccf367
--- /dev/null
+++ b/src/Installing/InstallForPhpProject/InstallPiePackageFromPath.php
@@ -0,0 +1,53 @@
+writeln(sprintf('Installing PIE extension from %s', $piePackagePath));
+ $pieJsonEditor
+ ->ensureExists()
+ ->addRepository('path', $piePackagePath);
+
+ try {
+ return ($this->invokeSubCommand)(
+ $invokeContext,
+ [
+ 'command' => 'install',
+ 'requested-package-and-version' => $pieRootPackage->getName() . ':*@dev',
+ ],
+ $input,
+ $output,
+ );
+ } finally {
+ $output->writeln(
+ sprintf(
+ 'Removing temporary path repository: %s',
+ $piePackagePath,
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+ $pieJsonEditor->removeRepository($piePackagePath);
+ }
+ }
+}
diff --git a/test/behaviour/Installing/InstallForPhpProject/InstallPiePackageFromPathTest.php b/test/behaviour/Installing/InstallForPhpProject/InstallPiePackageFromPathTest.php
new file mode 100644
index 00000000..ec36ff6e
--- /dev/null
+++ b/test/behaviour/Installing/InstallForPhpProject/InstallPiePackageFromPathTest.php
@@ -0,0 +1,125 @@
+command = $this->createMock(Command::class);
+ $this->rootPackage = new RootPackage('foo/bar', '1.2.3.0', '1.2.3');
+ $this->invokeSubCommand = $this->createMock(InvokeSubCommand::class);
+ $this->pieJsonEditor = $this->createMock(PieJsonEditor::class);
+ $this->input = $this->createMock(InputInterface::class);
+ $this->output = new BufferedOutput();
+ }
+
+ public function testInvokeWithSuccessfulSubCommand(): void
+ {
+ $this->pieJsonEditor
+ ->expects(self::once())
+ ->method('ensureExists')
+ ->willReturnSelf();
+ $this->pieJsonEditor
+ ->expects(self::once())
+ ->method('addRepository')
+ ->with('path', '/path/to/pie/package');
+ $this->pieJsonEditor
+ ->expects(self::once())
+ ->method('removeRepository')
+ ->with('/path/to/pie/package');
+
+ $this->invokeSubCommand->expects(self::once())
+ ->method('__invoke')
+ ->with(
+ $this->command,
+ [
+ 'command' => 'install',
+ 'requested-package-and-version' => 'foo/bar:*@dev',
+ ],
+ $this->input,
+ $this->output,
+ )
+ ->willReturn(Command::SUCCESS);
+
+ self::assertSame(
+ Command::SUCCESS,
+ (new InstallPiePackageFromPath($this->invokeSubCommand))(
+ $this->command,
+ '/path/to/pie/package',
+ $this->rootPackage,
+ $this->pieJsonEditor,
+ $this->input,
+ $this->output,
+ ),
+ );
+ }
+
+ public function testInvokeWithSubCommandException(): void
+ {
+ $this->pieJsonEditor
+ ->expects(self::once())
+ ->method('ensureExists')
+ ->willReturnSelf();
+ $this->pieJsonEditor
+ ->expects(self::once())
+ ->method('addRepository')
+ ->with('path', '/path/to/pie/package');
+
+ // We still expect the package path to be removed even if there is an exception!
+ $this->pieJsonEditor
+ ->expects(self::once())
+ ->method('removeRepository')
+ ->with('/path/to/pie/package');
+
+ $this->invokeSubCommand->expects(self::once())
+ ->method('__invoke')
+ ->with(
+ $this->command,
+ [
+ 'command' => 'install',
+ 'requested-package-and-version' => 'foo/bar:*@dev',
+ ],
+ $this->input,
+ $this->output,
+ )
+ ->willThrowException(new RuntimeException('oh no'));
+
+ $install = new InstallPiePackageFromPath($this->invokeSubCommand);
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('oh no');
+ $install(
+ $this->command,
+ '/path/to/pie/package',
+ $this->rootPackage,
+ $this->pieJsonEditor,
+ $this->input,
+ $this->output,
+ );
+ }
+}
diff --git a/test/integration/Command/InstallExtensionsForProjectCommandTest.php b/test/integration/Command/InstallExtensionsForProjectCommandTest.php
index 36f9d3d3..5376f8ce 100644
--- a/test/integration/Command/InstallExtensionsForProjectCommandTest.php
+++ b/test/integration/Command/InstallExtensionsForProjectCommandTest.php
@@ -9,20 +9,28 @@
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\FindMatchingPackages;
use Php\Pie\Installing\InstallForPhpProject\FindRootPackage;
+use Php\Pie\Installing\InstallForPhpProject\InstallPiePackageFromPath;
use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
+use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\BufferedOutput;
+use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
+use function getcwd;
+
#[CoversClass(InstallExtensionsForProjectCommand::class)]
final class InstallExtensionsForProjectCommandTest extends TestCase
{
@@ -31,6 +39,7 @@ final class InstallExtensionsForProjectCommandTest extends TestCase
private FindMatchingPackages&MockObject $findMatchingPackages;
private InstallSelectedPackage&MockObject $installSelectedPackage;
private QuestionHelper&MockObject $questionHelper;
+ private InstallPiePackageFromPath&MockObject $installPiePackage;
public function setUp(): void
{
@@ -57,12 +66,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);
$cmd = new InstallExtensionsForProjectCommand(
$this->findRootpackage,
$this->findMatchingPackages,
$this->installSelectedPackage,
+ $this->installPiePackage,
$container,
);
$cmd->setHelperSet(new HelperSet([
@@ -71,7 +82,7 @@ function (string $service): mixed {
$this->commandTester = new CommandTester($cmd);
}
- public function testInstallingExtensionsForProject(): void
+ public function testInstallingExtensionsForPhpProject(): void
{
$rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3');
$rootPackage->setRequires([
@@ -103,4 +114,31 @@ public function testInstallingExtensionsForProject(): void
self::assertStringContainsString('requires: standard ✅ Already installed', $outputString);
self::assertStringContainsString('requires: 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->installPiePackage
+ ->expects(self::once())
+ ->method('__invoke')
+ ->with(
+ self::isInstanceOf(InstallExtensionsForProjectCommand::class),
+ getcwd(),
+ $rootPackage,
+ self::isInstanceOf(PieJsonEditor::class),
+ self::isInstanceOf(InputInterface::class),
+ self::isInstanceOf(OutputInterface::class),
+ )
+ ->willReturn(Command::SUCCESS);
+
+ $this->commandTester->execute(
+ [],
+ ['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE],
+ );
+
+ $this->commandTester->assertCommandIsSuccessful();
+ }
}