diff --git a/bin/pie b/bin/pie index b85fce44..d8504461 100755 --- a/bin/pie +++ b/bin/pie @@ -13,6 +13,7 @@ use Php\Pie\Command\InstallExtensionsForProjectCommand; use Php\Pie\Command\RepositoryAddCommand; use Php\Pie\Command\RepositoryListCommand; use Php\Pie\Command\RepositoryRemoveCommand; +use Php\Pie\Command\SearchCommand; use Php\Pie\Command\SelfUpdateCommand; use Php\Pie\Command\SelfVerifyCommand; use Php\Pie\Command\ShowCommand; @@ -52,6 +53,7 @@ $application->setCommandLoader(new ContainerCommandLoader( 'upgrade' => UpgradeCommand::class, 'info' => InfoCommand::class, 'show' => ShowCommand::class, + 'search' => SearchCommand::class, 'repository:list' => RepositoryListCommand::class, 'repository:add' => RepositoryAddCommand::class, 'repository:remove' => RepositoryRemoveCommand::class, diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 6bf0f0f5..f9f7e964 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -58,7 +58,11 @@ use const PHP_VERSION; -/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ +/** + * @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks + * + * @phpstan-import-type MatchingPackages from FindMatchingPackages + */ final class CommandHelper { public const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version'; @@ -533,32 +537,10 @@ public static function handlePackageNotFound( $io->writeError($exception->getMessage()); try { - $matches = array_map( - static function (array $match) use ($io, $pieComposer): array { - $composerMatchingPackage = $pieComposer->getRepositoryManager()->findPackage($match['name'], '*'); - - // Attempts to augment the Composer packages found with the PIE extension name - if ($composerMatchingPackage instanceof CompletePackageInterface) { - try { - $match['extension-name'] = Package - ::fromComposerCompletePackage($composerMatchingPackage) - ->extensionName() - ->name(); - } catch (Throwable $t) { - $io->writeError( - sprintf( - 'Tried looking up extension name for %s, but failed: %s', - $match['name'], - $t->getMessage(), - ), - verbosity: IOInterface::VERY_VERBOSE, - ); - } - } - - return $match; - }, + $matches = self::augmentMatchesWithExtensionName( + $pieComposer, $findMatchingPackages->bySearching($pieComposer, $requestedPackageName), + $io, ); if (count($matches)) { @@ -596,6 +578,43 @@ static function (array $match) use ($io): void { return 1; } + /** + * Attempts to augment the given Composer package matches with the PIE extension name, where resolvable. + * + * @param MatchingPackages $matches + * + * @return MatchingPackages + */ + public static function augmentMatchesWithExtensionName(Composer $pieComposer, array $matches, IOInterface $io): array + { + return array_map( + static function (array $match) use ($io, $pieComposer): array { + $composerMatchingPackage = $pieComposer->getRepositoryManager()->findPackage($match['name'], '*'); + + if ($composerMatchingPackage instanceof CompletePackageInterface) { + try { + $match['extension-name'] = Package + ::fromComposerCompletePackage($composerMatchingPackage) + ->extensionName() + ->name(); + } catch (Throwable $t) { + $io->writeError( + sprintf( + 'Tried looking up extension name for %s, but failed: %s', + $match['name'], + $t->getMessage(), + ), + verbosity: IOInterface::VERY_VERBOSE, + ); + } + } + + return $match; + }, + $matches, + ); + } + public static function applyNoCacheOptionIfSet(InputInterface $input, IOInterface $io): void { if (! $input->hasOption(self::OPTION_NO_CACHE) || ! $input->getOption(self::OPTION_NO_CACHE)) { diff --git a/src/Command/SearchCommand.php b/src/Command/SearchCommand.php new file mode 100644 index 00000000..c601c63e --- /dev/null +++ b/src/Command/SearchCommand.php @@ -0,0 +1,100 @@ +addArgument( + self::ARG_SEARCH_TERM, + InputArgument::REQUIRED | InputArgument::IS_ARRAY, + 'The term(s) to search for, matched against package name, description and keywords.', + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io); + + CommandHelper::applyNoCacheOptionIfSet($input, $this->io); + + $searchTerms = $input->getArgument(self::ARG_SEARCH_TERM); + Assert::isArray($searchTerms); + Assert::allStringNotEmpty($searchTerms); + + $searchTerm = implode(' ', $searchTerms); + Assert::stringNotEmpty($searchTerm); + + $composer = PieComposerFactory::createPieComposer( + $this->container, + PieComposerRequest::noOperation( + $this->io, + $targetPlatform, + ), + ); + + try { + $matches = $this->findMatchingPackages->bySearching($composer, $searchTerm); + } catch (OutOfRangeException) { + $this->io->write(sprintf('No packages found matching "%s".', $searchTerm)); + + return Command::SUCCESS; + } + + $matches = CommandHelper::augmentMatchesWithExtensionName($composer, $matches, $this->io); + + $this->io->write(sprintf("\nFound %d package(s) matching \"%s\":", count($matches), $searchTerm)); + foreach ($matches as $match) { + $this->io->write(sprintf( + ' - %s%s: %s', + $match['name'], + array_key_exists('extension-name', $match) && is_string($match['extension-name']) + ? ' (provides extension: ' . $match['extension-name'] . ')' + : '', + $match['description'] ?? 'no description available', + )); + } + + return Command::SUCCESS; + } +} diff --git a/src/Container.php b/src/Container.php index bf547bf3..ab4da933 100644 --- a/src/Container.php +++ b/src/Container.php @@ -22,6 +22,7 @@ use Php\Pie\Command\RepositoryAddCommand; use Php\Pie\Command\RepositoryListCommand; use Php\Pie\Command\RepositoryRemoveCommand; +use Php\Pie\Command\SearchCommand; use Php\Pie\Command\SelfUpdateCommand; use Php\Pie\Command\SelfVerifyCommand; use Php\Pie\Command\ShowCommand; @@ -118,6 +119,7 @@ static function (ConsoleCommandEvent $event) use (&$displayedBanner, $io): void $container->singleton(UpgradeCommand::class); $container->singleton(InfoCommand::class); $container->singleton(ShowCommand::class); + $container->singleton(SearchCommand::class); $container->singleton(RepositoryListCommand::class); $container->singleton(RepositoryAddCommand::class); $container->singleton(RepositoryRemoveCommand::class); diff --git a/test/integration/Command/SearchCommandTest.php b/test/integration/Command/SearchCommandTest.php new file mode 100644 index 00000000..1f2e8cbf --- /dev/null +++ b/test/integration/Command/SearchCommandTest.php @@ -0,0 +1,43 @@ +commandTester = new CommandTester(Container::testFactory()->get(SearchCommand::class)); + } + + public function testSearchFindsMatchingPackages(): void + { + $this->commandTester->execute(['search-term' => ['example-pie-extension']]); + + $this->commandTester->assertCommandIsSuccessful(); + + $outputString = $this->commandTester->getDisplay(); + self::assertStringContainsString('asgrim/example-pie-extension', $outputString); + self::assertStringContainsString('Example PIE extension', $outputString); + self::assertStringContainsString('provides extension: example_pie_extension', $outputString); + } + + public function testSearchWithNoMatchesReturnsSuccessWithMessage(): void + { + $this->commandTester->execute(['search-term' => ['this-is-an-extension-all-about-happy-things-but-does-not-really-exist']]); + + $this->commandTester->assertCommandIsSuccessful(); + + $outputString = $this->commandTester->getDisplay(); + self::assertStringContainsString('No packages found matching "this-is-an-extension-all-about-happy-things-but-does-not-really-exist".', $outputString); + } +}