Repository: php/pie (php)

Issue:
Improve messaging when we can't find a package matching the constraint
When I run `bin/pie download xdebug/xdebug:3.4.0alpha1`, I see:

```
Unable to find an installable package xdebug/xdebug for version 3.4.0alpha1  
```

> It does not tell me *why* it can't find it, as there is certainly a release on [packagist](https://packagist.org/packages/xdebug/xdebug#3.4.0alpha1).

When you run `bin/pie download xdebug/xdebug:3.4.0alpha1@alpha` this works.

In this case, it is because the default stability flag is `stable`. This may be because we're missing something in version resolution in Composer, but either way, this could be improved with better messaging.

Pull request description:
Added composer problems to output when unable to find installable ext
Fixes #15 

Patch:
diff --git a/src/DependencyResolver/ArrayCollectionIO.php b/src/DependencyResolver/ArrayCollectionIO.php
new file mode 100644
index 0000000..caa7e95
--- /dev/null
+++ b/src/DependencyResolver/ArrayCollectionIO.php
@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Php\Pie\DependencyResolver;
+
+use Composer\IO\BaseIO;
+
+use function array_merge;
+use function is_array;
+
+class ArrayCollectionIO extends BaseIO
+{
+    /** @var string[] */
+    public array $errors = [];
+
+    public function isInteractive(): bool
+    {
+        return false;
+    }
+
+    public function isVerbose(): bool
+    {
+        return true;
+    }
+
+    public function isVeryVerbose(): bool
+    {
+        return true;
+    }
+
+    public function isDebug(): bool
+    {
+        return true;
+    }
+
+    public function isDecorated(): bool
+    {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public function write($messages, bool $newline = true, int $verbosity = self::NORMAL): void
+    {
+    }
+
+    /** {@inheritDoc} */
+    public function writeError($messages, bool $newline = true, int $verbosity = self::NORMAL): void
+    {
+        if (is_array($messages)) {
+            $this->errors = array_merge($this->errors, $messages);
+
+            return;
+        }
+
+        $this->errors[] = $messages;
+    }
+
+    /** {@inheritDoc} */
+    public function overwrite($messages, bool $newline = true, int|null $size = null, int $verbosity = self::NORMAL): void
+    {
+    }
+
+    /** {@inheritDoc} */
+    public function overwriteError($messages, bool $newline = true, int|null $size = null, int $verbosity = self::NORMAL): void
+    {
+    }
+
+    /** {@inheritDoc} */
+    public function ask(string $question, $default = null): void
+    {
+    }
+
+    public function askConfirmation(string $question, bool $default = true): void
+    {
+    }
+
+    /** {@inheritDoc} */
+    public function askAndValidate(string $question, callable $validator, int|null $attempts = null, $default = null): void
+    {
+    }
+
+    public function askAndHideAnswer(string $question): void
+    {
+    }
+
+    /** {@inheritDoc} */
+    public function select(string $question, array $choices, $default, $attempts = false, string $errorMessage = 'Value "%s" is invalid', bool $multiselect = false): void
+    {
+    }
+}
diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php
index 3b6f10d..e022999 100644
--- a/src/DependencyResolver/ResolveDependencyWithComposer.php
+++ b/src/DependencyResolver/ResolveDependencyWithComposer.php
@@ -35,14 +35,16 @@ final class ResolveDependencyWithComposer implements DependencyResolver
 
     public function __invoke(TargetPlatform $targetPlatform, string $packageName, string|null $requestedVersion): Package
     {
+        $io = new ArrayCollectionIO();
+
         $package = (new VersionSelector(
             $this->factoryRepositorySet($requestedVersion),
             ($this->resolveTargetPhpToPlatformRepository)($targetPlatform->phpBinaryPath),
         ))
-            ->findBestCandidate($packageName, $requestedVersion);
+            ->findBestCandidate($packageName, $requestedVersion, io: $io);
 
         if (! $package instanceof CompletePackageInterface) {
-            throw UnableToResolveRequirement::fromRequirement($packageName, $requestedVersion);
+            throw UnableToResolveRequirement::fromRequirement($packageName, $requestedVersion, $io);
         }
 
         /**
diff --git a/src/DependencyResolver/UnableToResolveRequirement.php b/src/DependencyResolver/UnableToResolveRequirement.php
index 1815eef..a6c4838 100644
--- a/src/DependencyResolver/UnableToResolveRequirement.php
+++ b/src/DependencyResolver/UnableToResolveRequirement.php
@@ -7,16 +7,26 @@ namespace Php\Pie\DependencyResolver;
 use Composer\Package\PackageInterface;
 use RuntimeException;
 
+use function array_map;
+use function count;
+use function implode;
 use function sprintf;
+use function strip_tags;
 
 class UnableToResolveRequirement extends RuntimeException
 {
-    public static function fromRequirement(string $requiredPackageName, string|null $requiredVersion): self
-    {
+    public static function fromRequirement(
+        string $requiredPackageName,
+        string|null $requiredVersion,
+        ArrayCollectionIO $io,
+    ): self {
+        $errors = $io->errors;
+
         return new self(sprintf(
-            'Unable to find an installable package %s%s',
+            'Unable to find an installable package %s%s%s',
             $requiredPackageName,
             $requiredVersion !== null ? sprintf(' for version %s.', $requiredVersion) : '.',
+            count($errors) ? "\n\n" . implode("\n\n", array_map(static fn ($e) => strip_tags($e), $errors)) : '',
         ));
     }
 


Test patch:
diff --git a/test/unit/DependencyResolver/UnableToResolveRequirementTest.php b/test/unit/DependencyResolver/UnableToResolveRequirementTest.php
index e79b3ae..654573f 100644
--- a/test/unit/DependencyResolver/UnableToResolveRequirementTest.php
+++ b/test/unit/DependencyResolver/UnableToResolveRequirementTest.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace Php\PieUnitTest\DependencyResolver;
 
 use Composer\Package\PackageInterface;
+use Php\Pie\DependencyResolver\ArrayCollectionIO;
 use Php\Pie\DependencyResolver\UnableToResolveRequirement;
 use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
@@ -34,15 +35,23 @@ final class UnableToResolveRequirementTest extends TestCase
 
     public function testFromRequirementWithVersion(): void
     {
-        $exception = UnableToResolveRequirement::fromRequirement('foo/bar', '^1.2');
+        $io = new ArrayCollectionIO();
+        $io->writeError('message1');
+        $io->writeError(['message2', 'message3']);
 
-        self::assertSame('Unable to find an installable package foo/bar for version ^1.2.', $exception->getMessage());
+        $exception = UnableToResolveRequirement::fromRequirement('foo/bar', '^1.2', $io);
+
+        self::assertSame("Unable to find an installable package foo/bar for version ^1.2.\n\nmessage1\n\nmessage2\n\nmessage3", $exception->getMessage());
     }
 
     public function testFromRequirementWithoutVersion(): void
     {
-        $exception = UnableToResolveRequirement::fromRequirement('foo/bar', null);
+        $io = new ArrayCollectionIO();
+        $io->writeError('message1');
+        $io->writeError(['message2', 'message3']);
+
+        $exception = UnableToResolveRequirement::fromRequirement('foo/bar', null, $io);
 
-        self::assertSame('Unable to find an installable package foo/bar.', $exception->getMessage());
+        self::assertSame("Unable to find an installable package foo/bar.\n\nmessage1\n\nmessage2\n\nmessage3", $exception->getMessage());
     }
 }