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
8 changes: 1 addition & 7 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
use Composer\Factory as ComposerFactory;
use Composer\IO\ConsoleIO;
use Composer\IO\IOInterface;
use Composer\Repository\CompositeRepository;
use Composer\Repository\RepositorySet;
use Composer\Util\AuthHelper;
use Composer\Util\Platform;
use GuzzleHttp\Client;
Expand Down Expand Up @@ -79,12 +77,8 @@ public static function factory(): ContainerInterface
$container->singleton(
DependencyResolver::class,
static function (ContainerInterface $container): DependencyResolver {
$composer = $container->get(Composer::class);
$repositorySet = new RepositorySet();
$repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories()));

return new ResolveDependencyWithComposer(
$repositorySet,
$container->get(Composer::class),
new ResolveTargetPhpToPlatformRepository(),
);
},
Expand Down
47 changes: 38 additions & 9 deletions src/DependencyResolver/ResolveDependencyWithComposer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,74 @@

namespace Php\Pie\DependencyResolver;

use Composer\Composer;
use Composer\Package\CompletePackageInterface;
use Composer\Package\Version\VersionSelector;
use Composer\Repository\CompositeRepository;
use Composer\Repository\RepositorySet;
use Php\Pie\ExtensionType;
use Php\Pie\Platform\TargetPhp\ResolveTargetPhpToPlatformRepository;
use Php\Pie\Platform\TargetPlatform;

use function preg_match;
use function str_starts_with;

/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
final class ResolveDependencyWithComposer implements DependencyResolver
{
public function __construct(
private readonly RepositorySet $repositorySet,
private readonly Composer $composer,
private readonly ResolveTargetPhpToPlatformRepository $resolveTargetPhpToPlatformRepository,
) {
}

public function __invoke(TargetPlatform $targetPlatform, string $packageName, string|null $requestedVersion): Package
private function factoryRepositorySet(string|null $requestedVersion): RepositorySet
{
$preferredStability = 'stable';
$repoSetFlags = 0;
$minimumStability = 'stable';

/** Stability options from {@see https://getcomposer.org/doc/04-schema.md#minimum-stability} */
if ($requestedVersion !== null && preg_match('#@(dev|alpha|beta|RC|stable)$#', $requestedVersion, $matches)) {
$preferredStability = $matches[1];
$repoSetFlags |= RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES;
if ($requestedVersion !== null) {
if (preg_match('#@(dev|alpha|beta|RC|stable)$#', $requestedVersion, $matches)) {
$minimumStability = $matches[1];
}

// If a specific stability was not requested, but the version requested was `dev-` something, change to dev min stability
if (! $matches && str_starts_with($requestedVersion, 'dev-')) {
$minimumStability = 'dev';
}
}

$repositorySet = new RepositorySet($minimumStability);
$repositorySet->addRepository(new CompositeRepository($this->composer->getRepositoryManager()->getRepositories()));

return $repositorySet;
}

public function __invoke(TargetPlatform $targetPlatform, string $packageName, string|null $requestedVersion): Package
{
$package = (new VersionSelector(
$this->repositorySet,
$this->factoryRepositorySet($requestedVersion),
($this->resolveTargetPhpToPlatformRepository)($targetPlatform->phpBinaryPath),
))
->findBestCandidate($packageName, $requestedVersion, $preferredStability, null, $repoSetFlags);
->findBestCandidate($packageName, $requestedVersion);

if (! $package instanceof CompletePackageInterface) {
throw UnableToResolveRequirement::fromRequirement($packageName, $requestedVersion);
}

/**
* If a specific commit hash is requested, override the references in the package. This is approximately what
* Composer does anyway:
*
* > ArrayLoader::parseLinks is in charge of this, it drops commit refs, for package resolution purposes we
* > only use dev-main, but we ensure in the PoolBuilder that root references (#...) are set so the dev-main
* > package has its source and dist refs overridden to be whatever you specify and that applies at install
* > time then but package metadata is only read from the branch's head
*/
if ($requestedVersion !== null && preg_match('/#([a-f0-9]{40})$/', $requestedVersion, $matches)) {
$package->setSourceDistReferences($matches[1]);
}

if (! ExtensionType::isValid($package->getType())) {
throw UnableToResolveRequirement::toPhpOrZendExtension($package, $packageName, $requestedVersion);
}
Expand Down
37 changes: 31 additions & 6 deletions test/integration/Command/DownloadCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public function setUp(): void
}

/**
* Note: this data provider is not intended to provide a fully comprehensive list of supported version mappings
* since it is slightly slower to run (as it actually downloads). For a fuller list of version resolution tests,
* please see {@see \Php\PieIntegrationTest\DependencyResolver\ResolveDependencyWithComposerTest}, which is much
* faster to execute!
*
* @return array<non-empty-string, array{0: non-empty-string, 1: non-empty-string}>
*
* @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131
Expand All @@ -46,11 +51,7 @@ public static function validVersionsList(): array
[self::TEST_PACKAGE . ':1.0.1-alpha.3@alpha', self::TEST_PACKAGE . ':1.0.1-alpha.3'],
[self::TEST_PACKAGE . ':*', self::TEST_PACKAGE . ':1.0.1'],
[self::TEST_PACKAGE . ':~1.0.0@alpha', self::TEST_PACKAGE . ':1.0.1'],
[self::TEST_PACKAGE . ':^1.1.0@alpha', self::TEST_PACKAGE . ':1.1.0-alpha.4'],
[self::TEST_PACKAGE . ':~1.0.0', self::TEST_PACKAGE . ':1.0.1'],
// @todo https://github.com/php/pie/issues/13 - in theory, these could work, on NonWindows at least
// [self::TEST_PACKAGE . ':dev-main', self::TEST_PACKAGE . ':???'],
// [self::TEST_PACKAGE . ':dev-main#769f906413d6d1e12152f6d34134cbcd347ca253', self::TEST_PACKAGE . ':???'],
];

return array_combine(
Expand All @@ -60,8 +61,10 @@ public static function validVersionsList(): array
}

#[DataProvider('validVersionsList')]
public function testDownloadCommandWillDownloadCompatibleExtension(string $requestedVersion, string $expectedVersion): void
{
public function testDownloadCommandWillDownloadCompatibleExtension(
string $requestedVersion,
string $expectedVersion,
): void {
if (PHP_VERSION_ID < 80300 || PHP_VERSION_ID >= 80400) {
self::markTestSkipped('This test can only run on PHP 8.3 - you are running ' . PHP_VERSION);
}
Expand All @@ -75,6 +78,28 @@ public function testDownloadCommandWillDownloadCompatibleExtension(string $reque
self::assertStringContainsString('Extracted ' . $expectedVersion . ' source to', $outputString);
}

public function testDownloadCommandWillDownloadSpecificCommits(): void
{
if (Platform::isWindows()) {
self::markTestSkipped('This test can only run on non-Windows systems');
}

if (PHP_VERSION_ID < 80300 || PHP_VERSION_ID >= 80400) {
self::markTestSkipped('This test can only run on PHP 8.3 - you are running ' . PHP_VERSION);
}

$this->commandTester->execute(['requested-package-and-version' => 'asgrim/example-pie-extension:dev-main#9b5e6c80a1e05556e4e6824f0c112a4992cee001']);

$this->commandTester->assertCommandIsSuccessful();

$outputString = $this->commandTester->getDisplay();
self::assertStringContainsString('Found package: asgrim/example-pie-extension:dev-main which provides', $outputString);
self::assertStringMatchesFormat(
'%AExtracted asgrim/example-pie-extension:dev-main source to: /tmp/%s/asgrim-example-pie-extension-9b5e6c8%A',
$outputString,
);
}

#[DataProvider('validVersionsList')]
public function testDownloadingWithPhpConfig(string $requestedVersion, string $expectedVersion): void
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Php\PieIntegrationTest\DependencyResolver;

use Php\Pie\Container;
use Php\Pie\DependencyResolver\DependencyResolver;
use Php\Pie\DependencyResolver\ResolveDependencyWithComposer;
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
use Php\Pie\Platform\TargetPlatform;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

use function array_combine;
use function array_map;

use const PHP_VERSION;
use const PHP_VERSION_ID;

#[CoversClass(ResolveDependencyWithComposer::class)]
final class ResolveDependencyWithComposerTest extends TestCase
{
private const DOWNLOAD_URL_ANY = 'https://api.github.com/repos/asgrim/example-pie-extension/zipball/%s';
private const DOWNLOAD_URL_1_0_1_ALPHA_3 = 'https://api.github.com/repos/asgrim/example-pie-extension/zipball/115f8f8e01ee098a18ec2f47af4852be51ebece7';
private const DOWNLOAD_URL_1_0_1 = 'https://api.github.com/repos/asgrim/example-pie-extension/zipball/769f906413d6d1e12152f6d34134cbcd347ca253';
private const DOWNLOAD_URL_1_1_0_BETA_1 = 'https://api.github.com/repos/asgrim/example-pie-extension/zipball/b8cec47269dc607b3111fbebd2c47f5b5112595e';

/**
* @return array<non-empty-string|'null', array{0: non-empty-string|null, 1: non-empty-string, 2: non-empty-string}>
*
* @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131
*/
public static function validVersionsList(): array
{
$versionsAndExpected = [
[null, '1.0.1', self::DOWNLOAD_URL_ANY],
['*', '1.0.1', self::DOWNLOAD_URL_ANY],
['1.0.1-alpha.3@alpha', '1.0.1-alpha.3', self::DOWNLOAD_URL_1_0_1_ALPHA_3],
['^1.0', '1.0.1', self::DOWNLOAD_URL_1_0_1],
['^1.1.0@alpha', '1.1.0-beta.1', self::DOWNLOAD_URL_1_1_0_BETA_1],
['^1.0@beta', '1.0.1', self::DOWNLOAD_URL_1_0_1],
['^1.1@beta', '1.1.0-beta.1', self::DOWNLOAD_URL_1_1_0_BETA_1],
['~1.0.0', '1.0.1', self::DOWNLOAD_URL_1_0_1],
['~1.0.0@alpha', '1.0.1', self::DOWNLOAD_URL_1_0_1],
['~1.0.0@beta', '1.0.1', self::DOWNLOAD_URL_1_0_1],
['~1.0@beta', '1.0.1', self::DOWNLOAD_URL_1_0_1],
['dev-main', 'dev-main', self::DOWNLOAD_URL_ANY],
['dev-main#769f906413d6d1e12152f6d34134cbcd347ca253', 'dev-main', self::DOWNLOAD_URL_1_0_1],
];

return array_combine(
array_map(static fn ($item) => $item[0] ?? 'null', $versionsAndExpected),
$versionsAndExpected,
);
}

#[DataProvider('validVersionsList')]
public function testDependenciesAreResolvedToExpectedVersions(
string|null $requestedVersion,
string $expectedVersion,
string $expectedDownloadUrl,
): void {
if (PHP_VERSION_ID < 80300 || PHP_VERSION_ID >= 80400) {
self::markTestSkipped('This test can only run on PHP 8.3 - you are running ' . PHP_VERSION);
}

$container = Container::factory();
$resolve = $container->get(DependencyResolver::class);

$package = $resolve->__invoke(
TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess()),
'asgrim/example-pie-extension',
$requestedVersion,
);

self::assertSame($expectedVersion, $package->version);
self::assertNotNull($package->downloadUrl);
self::assertStringMatchesFormat($expectedDownloadUrl, $package->downloadUrl);
}
}
18 changes: 12 additions & 6 deletions test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

namespace Php\PieUnitTest\DependencyResolver;

use Composer\Composer;
use Composer\IO\NullIO;
use Composer\Repository\CompositeRepository;
use Composer\Repository\RepositoryFactory;
use Composer\Repository\RepositorySet;
use Composer\Repository\RepositoryManager;
use Php\Pie\DependencyResolver\ResolveDependencyWithComposer;
use Php\Pie\DependencyResolver\UnableToResolveRequirement;
use Php\Pie\Platform\Architecture;
Expand All @@ -23,15 +24,20 @@
#[CoversClass(ResolveDependencyWithComposer::class)]
final class ResolveDependencyWithComposerTest extends TestCase
{
private RepositorySet $repositorySet;
private Composer $composer;
private ResolveTargetPhpToPlatformRepository $resolveTargetPhpToPlatformRepository;

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

$this->repositorySet = new RepositorySet();
$this->repositorySet->addRepository(new CompositeRepository(RepositoryFactory::defaultReposWithDefaultManager(new NullIO())));
$repoManager = $this->createMock(RepositoryManager::class);
$repoManager->method('getRepositories')
->willReturn([new CompositeRepository(RepositoryFactory::defaultReposWithDefaultManager(new NullIO()))]);

$this->composer = $this->createMock(Composer::class);
$this->composer->method('getRepositoryManager')
->willReturn($repoManager);

$this->resolveTargetPhpToPlatformRepository = new ResolveTargetPhpToPlatformRepository();
}
Expand All @@ -52,7 +58,7 @@ public function testPackageThatCanBeResolved(): void
);

$package = (new ResolveDependencyWithComposer(
$this->repositorySet,
$this->composer,
$this->resolveTargetPhpToPlatformRepository,
))($targetPlatform, 'asgrim/example-pie-extension', '^1.0');

Expand Down Expand Up @@ -94,7 +100,7 @@ public function testPackageThatCannotBeResolvedThrowsException(array $platformOv
$this->expectException(UnableToResolveRequirement::class);

(new ResolveDependencyWithComposer(
$this->repositorySet,
$this->composer,
$this->resolveTargetPhpToPlatformRepository,
))(
$targetPlatform,
Expand Down