diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 430ba14c..6582e98a 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -11,6 +11,9 @@
displayDetailsOnTestsThatTriggerWarnings="true"
failOnRisky="true"
failOnWarning="true">
+
+
+
test/unit
diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php
index 61987fd8..e1c91d6e 100644
--- a/src/Command/CommandHelper.php
+++ b/src/Command/CommandHelper.php
@@ -51,14 +51,15 @@
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
final class CommandHelper
{
- public const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version';
- public const OPTION_WITH_PHP_CONFIG = 'with-php-config';
- public const OPTION_WITH_PHP_PATH = 'with-php-path';
- public const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path';
- public const OPTION_WORKING_DIRECTORY = 'working-dir';
- private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs';
- private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension';
- private const OPTION_FORCE = 'force';
+ public const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version';
+ public const OPTION_WITH_PHP_CONFIG = 'with-php-config';
+ public const OPTION_WITH_PHP_PATH = 'with-php-path';
+ public const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path';
+ public const OPTION_WORKING_DIRECTORY = 'working-dir';
+ public const OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL = 'allow-non-interactive-project-install';
+ private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs';
+ private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension';
+ private const OPTION_FORCE = 'force';
/** @psalm-suppress UnusedConstructor */
private function __construct()
@@ -125,6 +126,13 @@ public static function configureDownloadBuildInstallOptions(Command $command, bo
self::configurePhpConfigOptions($command);
+ $command->addOption(
+ self::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL,
+ null,
+ InputOption::VALUE_NONE,
+ 'When installing a PHP project, allow non-interactive project installations. Only used in certain contexts.',
+ );
+
/**
* Allows additional options for the `./configure` command to be passed here.
* Note, this means you probably need to call {@see self::validateInput()} to validate the input manually...
diff --git a/src/Command/InstallExtensionsForProjectCommand.php b/src/Command/InstallExtensionsForProjectCommand.php
index 2f9131c0..c84f258a 100644
--- a/src/Command/InstallExtensionsForProjectCommand.php
+++ b/src/Command/InstallExtensionsForProjectCommand.php
@@ -17,6 +17,7 @@
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Php\Pie\Installing\InstallForPhpProject\InstallPiePackageFromPath;
use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage;
+use Php\Pie\Platform;
use Php\Pie\Platform\InstalledPiePackages;
use Php\Pie\Util\Emoji;
use Psr\Container\ContainerInterface;
@@ -30,13 +31,16 @@
use Symfony\Component\Console\Question\ChoiceQuestion;
use Throwable;
+use function array_column;
use function array_keys;
use function array_map;
use function array_merge;
use function array_walk;
use function assert;
use function chdir;
+use function count;
use function getcwd;
+use function implode;
use function in_array;
use function is_dir;
use function is_string;
@@ -123,6 +127,16 @@ public function execute(InputInterface $input, OutputInterface $output): int
return $exit;
}
+ $allowNonInteractive = $input->hasOption(CommandHelper::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL) && $input->getOption(CommandHelper::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL);
+ if (! Platform::isInteractive() && ! $allowNonInteractive) {
+ $output->writeln(sprintf(
+ 'Aborting! You are not running in interactive mode, and --%s was not specified.',
+ CommandHelper::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL,
+ ));
+
+ return Command::FAILURE;
+ }
+
$targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
$output->writeln(sprintf(
@@ -221,27 +235,46 @@ function (Link $link) use ($pieComposer, $phpEnabledExtensions, $installedPiePac
return;
}
- $choiceQuestion = new ChoiceQuestion(
- "\nThe following packages may be suitable, which would you like to install: ",
- array_merge(
- ['None'],
- array_map(
- static function (array $match): string {
- return sprintf('%s: %s', $match['name'], $match['description'] ?? 'no description available');
- },
- $matches,
+ if (! Platform::isInteractive() && count($matches) > 1) {
+ $anyErrorsHappened = true;
+
+ // @todo Figure out if there is a way to improve this, safely
+ $output->writeln(sprintf(
+ "Multiple packages were found for %s:\n %s\n\nThis means you cannot `pie install` this project interactively for now.",
+ $extension->nameWithExtPrefix(),
+ implode("\n ", array_column($matches, 'name')),
+ ));
+
+ return;
+ }
+
+ if (Platform::isInteractive()) {
+ $choiceQuestion = new ChoiceQuestion(
+ "\nThe following packages may be suitable, which would you like to install: ",
+ array_merge(
+ ['None'],
+ array_map(
+ static function (array $match): string {
+ return sprintf('%s: %s', $match['name'], $match['description'] ?? 'no description available');
+ },
+ $matches,
+ ),
),
- ),
- 0,
- );
+ 0,
+ );
- $selectedPackageAnswer = (string) $helper->ask($input, $output, $choiceQuestion);
+ $selectedPackageAnswer = (string) $helper->ask($input, $output, $choiceQuestion);
- if ($selectedPackageAnswer === 'None') {
- $output->writeln('Okay I won\'t install anything for ' . $extension->name());
- $anyErrorsHappened = true;
+ if ($selectedPackageAnswer === 'None') {
+ $output->writeln('Okay I won\'t install anything for ' . $extension->name());
+ $anyErrorsHappened = true;
- return;
+ return;
+ }
+
+ $selectedPackageName = substr($selectedPackageAnswer, 0, (int) strpos($selectedPackageAnswer, ':'));
+ } else {
+ $selectedPackageName = $matches[0]['name'];
}
$requestInstallConstraint = '';
@@ -250,8 +283,12 @@ static function (array $match): string {
}
try {
+ $output->writeln(
+ sprintf('Invoking pie install of %s%s', $selectedPackageName, $requestInstallConstraint),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
$this->installSelectedPackage->withPieCli(
- substr($selectedPackageAnswer, 0, (int) strpos($selectedPackageAnswer, ':')) . $requestInstallConstraint,
+ $selectedPackageName . $requestInstallConstraint,
$input,
$output,
);
diff --git a/test/integration/Command/InstallExtensionsForProjectCommandTest.php b/test/integration/Command/InstallExtensionsForProjectCommandTest.php
index f4d6c7b7..e05f5948 100644
--- a/test/integration/Command/InstallExtensionsForProjectCommandTest.php
+++ b/test/integration/Command/InstallExtensionsForProjectCommandTest.php
@@ -101,10 +101,6 @@ public function testInstallingExtensionsForPhpProject(): void
new Constraint('>=', '1.2.0.0-dev'),
new Constraint('<', '2.0.0.0-dev'),
]), Link::TYPE_REQUIRE, '^1.2'),
-// 'ext-mismatching' => new Link('my/project', 'ext-mismatching', new MultiConstraint([
-// new Constraint('>=', '2.0.0.0-dev'),
-// new Constraint('<', '3.0.0.0-dev'),
-// ]), Link::TYPE_REQUIRE, '^2.0'),
]);
$this->composerFactoryForProject->method('rootPackage')->willReturn($rootPackage);
@@ -119,20 +115,8 @@ public function testInstallingExtensionsForPhpProject(): void
$this->composerFactoryForProject->method('composer')->willReturn($composer);
-// $this->installedPiePackages->method('allPiePackages')->willReturn([
-// 'mismatching' => new Package(
-// $this->createMock(CompletePackageInterface::class),
-// ExtensionType::PhpModule,
-// ExtensionName::normaliseFromString('mismatching'),
-// 'vendor/mismatching',
-// '1.9.3',
-// null,
-// ),
-// ]);
-
$this->findMatchingPackages->method('for')->willReturn([
['name' => 'vendor1/foobar', 'description' => 'The official foobar implementation'],
- ['name' => 'vendor2/afoobar', 'description' => 'An improved async foobar extension'],
]);
$this->questionHelper->method('ask')->willReturn('vendor1/foobar: The official foobar implementation');
@@ -142,16 +126,63 @@ public function testInstallingExtensionsForPhpProject(): void
->with('vendor1/foobar:^1.2');
$this->commandTester->execute(
- [],
+ ['--allow-non-interactive-project-install' => true],
['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE],
);
$outputString = $this->commandTester->getDisplay();
- $this->commandTester->assertCommandIsSuccessful();
+ $this->commandTester->assertCommandIsSuccessful($outputString);
+ self::assertStringContainsString('Checking extensions for your project my/project', $outputString);
+ self::assertStringContainsString('requires: ext-standard:* ✅ Already installed', $outputString);
+ self::assertStringContainsString('requires: ext-foobar:^1.2 🚫 Missing', $outputString);
+ }
+
+ public function testInstallingExtensionsForPhpProjectWithMultipleMatches(): void
+ {
+ $rootPackage = new RootPackage('my/project', '1.2.3.0', '1.2.3');
+ $rootPackage->setRequires([
+ 'ext-standard' => new Link('my/project', 'ext-standard', new Constraint('=', '*'), Link::TYPE_REQUIRE, '*'),
+ 'ext-foobar' => new Link('my/project', 'ext-foobar', new MultiConstraint([
+ new Constraint('>=', '1.2.0.0-dev'),
+ new Constraint('<', '2.0.0.0-dev'),
+ ]), Link::TYPE_REQUIRE, '^1.2'),
+ ]);
+ $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'],
+ ['name' => 'vendor2/afoobar', 'description' => 'An improved async foobar extension'],
+ ]);
+
+ $this->questionHelper->method('ask')->willReturn('vendor1/foobar: The official foobar implementation');
+
+ $this->installSelectedPackage->expects(self::never())
+ ->method('withPieCli');
+
+ $this->commandTester->execute(
+ ['--allow-non-interactive-project-install' => true],
+ ['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE],
+ );
+
+ $outputString = $this->commandTester->getDisplay();
+
+ self::assertSame(Command::FAILURE, $this->commandTester->getStatusCode());
self::assertStringContainsString('Checking extensions for your project my/project', $outputString);
self::assertStringContainsString('requires: ext-standard:* ✅ Already installed', $outputString);
self::assertStringContainsString('requires: ext-foobar:^1.2 🚫 Missing', $outputString);
+ self::assertStringContainsString('Multiple packages were found for ext-foobar', $outputString);
}
public function testInstallingExtensionsForPieProject(): void