Repository: php/pie (php)

Issue:
Show command & related components
Design documentation: https://github.com/ThePHPF/pie-design/blob/main/README.md

Implement the "Show" command, and its related components:

 * List the installed modules and their versions

Definition of done:

 - A user is able to list installed modules and versions using a command like `bin/pie show`

Pull request description:
Show command
Fixes #5 

Patch:
diff --git a/bin/pie b/bin/pie
index 59435f7..bfd6d26 100755
--- a/bin/pie
+++ b/bin/pie
@@ -9,6 +9,7 @@ use Php\Pie\Command\BuildCommand;
 use Php\Pie\Command\DownloadCommand;
 use Php\Pie\Command\InfoCommand;
 use Php\Pie\Command\InstallCommand;
+use Php\Pie\Command\ShowCommand;
 use Symfony\Component\Console\Application;
 use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
 use Symfony\Component\Console\Input\InputInterface;
@@ -27,6 +28,7 @@ $application->setCommandLoader(new ContainerCommandLoader(
         'build' => BuildCommand::class,
         'install' => InstallCommand::class,
         'info' => InfoCommand::class,
+        'show' => ShowCommand::class,
     ]
 ));
 $application->run($container->get(InputInterface::class), $container->get(OutputInterface::class));
diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php
index 2cc0377..04c2422 100644
--- a/src/Command/BuildCommand.php
+++ b/src/Command/BuildCommand.php
@@ -30,7 +30,7 @@ final class BuildCommand extends Command
     {
         parent::configure();
 
-        CommandHelper::configureOptions($this);
+        CommandHelper::configureDownloadBuildInstallOptions($this);
     }
 
     public function execute(InputInterface $input, OutputInterface $output): int
diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php
index 78bef90..700f7ed 100644
--- a/src/Command/CommandHelper.php
+++ b/src/Command/CommandHelper.php
@@ -47,13 +47,8 @@ final class CommandHelper
     {
     }
 
-    public static function configureOptions(Command $command): void
+    public static function configurePhpConfigOptions(Command $command): void
     {
-        $command->addArgument(
-            self::ARG_REQUESTED_PACKAGE_AND_VERSION,
-            InputArgument::REQUIRED,
-            'The extension name and version constraint to use, in the format {ext-name}{?:{?version-constraint}{?@stability}}, for example `xdebug/xdebug:^3.4@alpha`, `xdebug/xdebug:@alpha`, `xdebug/xdebug:^3.4`, etc.',
-        );
         $command->addOption(
             self::OPTION_WITH_PHP_CONFIG,
             null,
@@ -66,6 +61,17 @@ final class CommandHelper
             InputOption::VALUE_OPTIONAL,
             'The path to the `php` binary to use as the target PHP platform on ' . OperatingSystem::Windows->asFriendlyName() . ', e.g. --' . self::OPTION_WITH_PHP_PATH . '=C:\usr\php7.4.33\php.exe',
         );
+    }
+
+    public static function configureDownloadBuildInstallOptions(Command $command): void
+    {
+        $command->addArgument(
+            self::ARG_REQUESTED_PACKAGE_AND_VERSION,
+            InputArgument::REQUIRED,
+            'The extension name and version constraint to use, in the format {ext-name}{?:{?version-constraint}{?@stability}}, for example `xdebug/xdebug:^3.4@alpha`, `xdebug/xdebug:@alpha`, `xdebug/xdebug:^3.4`, etc.',
+        );
+
+        self::configurePhpConfigOptions($command);
 
         /**
          * Allows additional options for the `./configure` command to be passed here.
diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php
index 9c28f60..ea9446e 100644
--- a/src/Command/DownloadCommand.php
+++ b/src/Command/DownloadCommand.php
@@ -30,7 +30,7 @@ final class DownloadCommand extends Command
     {
         parent::configure();
 
-        CommandHelper::configureOptions($this);
+        CommandHelper::configureDownloadBuildInstallOptions($this);
     }
 
     public function execute(InputInterface $input, OutputInterface $output): int
diff --git a/src/Command/InfoCommand.php b/src/Command/InfoCommand.php
index 0487b12..c250ac8 100644
--- a/src/Command/InfoCommand.php
+++ b/src/Command/InfoCommand.php
@@ -29,7 +29,7 @@ final class InfoCommand extends Command
     {
         parent::configure();
 
-        CommandHelper::configureOptions($this);
+        CommandHelper::configureDownloadBuildInstallOptions($this);
     }
 
     public function execute(InputInterface $input, OutputInterface $output): int
diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php
index aefd24d..3fc2bf5 100644
--- a/src/Command/InstallCommand.php
+++ b/src/Command/InstallCommand.php
@@ -33,7 +33,7 @@ final class InstallCommand extends Command
     {
         parent::configure();
 
-        CommandHelper::configureOptions($this);
+        CommandHelper::configureDownloadBuildInstallOptions($this);
     }
 
     public function execute(InputInterface $input, OutputInterface $output): int
diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php
new file mode 100644
index 0000000..8203b7a
--- /dev/null
+++ b/src/Command/ShowCommand.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Php\Pie\Command;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+use function array_walk;
+use function sprintf;
+
+#[AsCommand(
+    name: 'show',
+    description: 'List the installed modules and their versions.',
+)]
+final class ShowCommand extends Command
+{
+    public function configure(): void
+    {
+        parent::configure();
+
+        CommandHelper::configurePhpConfigOptions($this);
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
+
+        $extensions = $targetPlatform->phpBinaryPath->extensions();
+        $output->writeln("\n" . '<info>Loaded extensions:</info>');
+        array_walk(
+            $extensions,
+            static function (string $version, string $name) use ($output): void {
+                $output->writeln(sprintf('%s:%s', $name, $version));
+            },
+        );
+
+        return Command::SUCCESS;
+    }
+}
diff --git a/src/Container.php b/src/Container.php
index 56f9a59..30f3771 100644
--- a/src/Container.php
+++ b/src/Container.php
@@ -21,6 +21,7 @@ use Php\Pie\Command\BuildCommand;
 use Php\Pie\Command\DownloadCommand;
 use Php\Pie\Command\InfoCommand;
 use Php\Pie\Command\InstallCommand;
+use Php\Pie\Command\ShowCommand;
 use Php\Pie\DependencyResolver\DependencyResolver;
 use Php\Pie\DependencyResolver\ResolveDependencyWithComposer;
 use Php\Pie\Downloading\DownloadAndExtract;
@@ -54,6 +55,7 @@ final class Container
         $container->singleton(BuildCommand::class);
         $container->singleton(InstallCommand::class);
         $container->singleton(InfoCommand::class);
+        $container->singleton(ShowCommand::class);
 
         $container->singleton(IOInterface::class, static function (ContainerInterface $container): IOInterface {
             return new ConsoleIO(


Test patch:
diff --git a/test/integration/Command/ShowCommandTest.php b/test/integration/Command/ShowCommandTest.php
new file mode 100644
index 0000000..065ad08
--- /dev/null
+++ b/test/integration/Command/ShowCommandTest.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Php\PieIntegrationTest\Command;
+
+use Php\Pie\Command\ShowCommand;
+use Php\Pie\Container;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Tester\CommandTester;
+
+use function get_loaded_extensions;
+use function phpversion;
+
+#[CoversClass(ShowCommand::class)]
+final class ShowCommandTest extends TestCase
+{
+    private CommandTester $commandTester;
+
+    public function setUp(): void
+    {
+        $this->commandTester = new CommandTester(Container::factory()->get(ShowCommand::class));
+    }
+
+    public function testExecute(): void
+    {
+        $this->commandTester->execute([]);
+
+        $this->commandTester->assertCommandIsSuccessful();
+
+        $outputString = $this->commandTester->getDisplay();
+
+        foreach (get_loaded_extensions() as $extension) {
+            self::assertStringContainsString(
+                $extension . ':' . (phpversion($extension) === false ? '0' : phpversion($extension)),
+                $outputString,
+            );
+        }
+    }
+}
diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php
index bbdd5c0..3c38e08 100644
--- a/test/unit/Command/CommandHelperTest.php
+++ b/test/unit/Command/CommandHelperTest.php
@@ -182,7 +182,7 @@ final class CommandHelperTest extends TestCase
         $command = new Command();
         $input   = new ArrayInput(['--with-php-config' => 'C:\path\to\php-config']);
         $output  = new NullOutput();
-        CommandHelper::configureOptions($command);
+        CommandHelper::configureDownloadBuildInstallOptions($command);
         CommandHelper::validateInput($input, $command);
 
         $this->expectException(InvalidArgumentException::class);
@@ -199,7 +199,7 @@ final class CommandHelperTest extends TestCase
         $command = new Command();
         $input   = new ArrayInput(['--with-php-path' => '/usr/bin/php']);
         $output  = new NullOutput();
-        CommandHelper::configureOptions($command);
+        CommandHelper::configureDownloadBuildInstallOptions($command);
         CommandHelper::validateInput($input, $command);
 
         $this->expectException(InvalidArgumentException::class);