Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bin/pie
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
71 changes: 45 additions & 26 deletions src/Command/CommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)) {
Expand Down
100 changes: 100 additions & 0 deletions src/Command/SearchCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace Php\Pie\Command;

use Composer\IO\IOInterface;
use OutOfRangeException;
use Php\Pie\ComposerIntegration\PieComposerFactory;
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Webmozart\Assert\Assert;

use function array_key_exists;
use function count;
use function implode;
use function is_string;
use function sprintf;

#[AsCommand(
name: 'search',
description: 'Search for PIE-compatible packages by name or keyword.',
)]
final class SearchCommand extends Command
{
private const ARG_SEARCH_TERM = 'search-term';

public function __construct(
private readonly ContainerInterface $container,
private readonly FindMatchingPackages $findMatchingPackages,
private readonly IOInterface $io,
) {
parent::__construct();
}

public function configure(): void
{
parent::configure();

CommandHelper::configurePhpConfigOptions($this);

$this->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(
' - <info>%s</info>%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;
}
}
2 changes: 2 additions & 0 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
43 changes: 43 additions & 0 deletions test/integration/Command/SearchCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Php\PieIntegrationTest\Command;

use Php\Pie\Command\SearchCommand;
use Php\Pie\Container;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(SearchCommand::class)]
final class SearchCommandTest extends TestCase
{
private CommandTester $commandTester;

public function setUp(): void
{
$this->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);
}
}
Loading