diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php
index 9fe973a..4a61d51 100644
--- a/src/Command/DownloadCommand.php
+++ b/src/Command/DownloadCommand.php
@@ -79,15 +79,14 @@ public function execute(InputInterface $input, OutputInterface $output): int
$requestedNameAndVersionPair['version'],
);
- $output->writeln(sprintf('Found package: %s (version: %s)', $package->name, $package->version));
+ $output->writeln(sprintf('Found package: %s', $package->prettyNameAndVersion()));
$output->writeln(sprintf('Dist download URL: %s', $package->downloadUrl ?? '(none)'));
$downloadedPackage = ($this->downloadAndExtract)($package);
$output->writeln(sprintf(
- 'Extracted %s:%s source to: %s',
- $downloadedPackage->package->name,
- $downloadedPackage->package->version,
+ 'Extracted %s source to: %s',
+ $downloadedPackage->package->prettyNameAndVersion(),
$downloadedPackage->extractedSourcePath,
));
diff --git a/src/Container.php b/src/Container.php
index b3a954f..3839cc7 100644
--- a/src/Container.php
+++ b/src/Container.php
@@ -13,13 +13,17 @@
use Composer\Util\AuthHelper;
use Composer\Util\Platform;
use GuzzleHttp\Client;
+use GuzzleHttp\ClientInterface;
+use GuzzleHttp\RequestOptions;
use Illuminate\Container\Container as IlluminateContainer;
use Php\Pie\Command\DownloadCommand;
use Php\Pie\DependencyResolver\DependencyResolver;
use Php\Pie\DependencyResolver\ResolveDependencyWithComposer;
use Php\Pie\Downloading\DownloadAndExtract;
use Php\Pie\Downloading\DownloadZip;
-use Php\Pie\Downloading\ExtractZip;
+use Php\Pie\Downloading\DownloadZipWithGuzzle;
+use Php\Pie\Downloading\GithubPackageReleaseAssets;
+use Php\Pie\Downloading\PackageReleaseAssets;
use Php\Pie\Downloading\UnixDownloadAndExtract;
use Php\Pie\Downloading\WindowsDownloadAndExtract;
use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository;
@@ -74,39 +78,23 @@ static function (ContainerInterface $container): DependencyResolver {
);
},
);
- $container->singleton(
- UnixDownloadAndExtract::class,
- static function (ContainerInterface $container): UnixDownloadAndExtract {
- return new UnixDownloadAndExtract(
- new DownloadZip(
- new Client(),
- ),
- new ExtractZip(),
- new AuthHelper(
- $container->get(IOInterface::class),
- $container->get(Composer::class)->getConfig(),
- ),
- );
+ $container->bind(
+ ClientInterface::class,
+ static function (): ClientInterface {
+ return new Client([RequestOptions::HTTP_ERRORS => false]);
},
);
$container->singleton(
- WindowsDownloadAndExtract::class,
- static function (ContainerInterface $container): WindowsDownloadAndExtract {
- $guzzleClient = new Client();
-
- return new WindowsDownloadAndExtract(
- new DownloadZip(
- $guzzleClient,
- ),
- new ExtractZip(),
- new AuthHelper(
- $container->get(IOInterface::class),
- $container->get(Composer::class)->getConfig(),
- ),
- $guzzleClient,
+ AuthHelper::class,
+ static function (ContainerInterface $container): AuthHelper {
+ return new AuthHelper(
+ $container->get(IOInterface::class),
+ $container->get(Composer::class)->getConfig(),
);
},
);
+ $container->alias(DownloadZipWithGuzzle::class, DownloadZip::class);
+ $container->alias(GithubPackageReleaseAssets::class, PackageReleaseAssets::class);
$container->singleton(
DownloadAndExtract::class,
static function (ContainerInterface $container): DownloadAndExtract {
diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php
index d6c1151..0551e72 100644
--- a/src/DependencyResolver/Package.php
+++ b/src/DependencyResolver/Package.php
@@ -31,4 +31,9 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com
$completePackage->getDistUrl(),
);
}
+
+ public function prettyNameAndVersion(): string
+ {
+ return $this->name . ':' . $this->version;
+ }
}
diff --git a/src/Downloading/DownloadZip.php b/src/Downloading/DownloadZip.php
index 4b13acc..0f9b0b5 100644
--- a/src/Downloading/DownloadZip.php
+++ b/src/Downloading/DownloadZip.php
@@ -4,42 +4,15 @@
namespace Php\Pie\Downloading;
-use GuzzleHttp\ClientInterface;
-use GuzzleHttp\RequestOptions;
use Psr\Http\Message\RequestInterface;
-use Psr\Http\Message\ResponseInterface;
-
-use function assert;
-use function file_put_contents;
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
-final class DownloadZip
+interface DownloadZip
{
- public function __construct(
- private readonly ClientInterface $client,
- ) {
- }
-
- public function downloadZipAndReturnLocalPath(RequestInterface $request, string $localPath): string
- {
- $response = $this->client
- ->sendAsync(
- $request,
- [
- RequestOptions::ALLOW_REDIRECTS => true,
- RequestOptions::HTTP_ERRORS => false,
- RequestOptions::SYNCHRONOUS => true,
- ],
- )
- ->wait();
- assert($response instanceof ResponseInterface);
-
- // @todo check response was successful
-
- // @todo handle this writing better
- $tmpZipFile = $localPath . '/downloaded.zip';
- file_put_contents($tmpZipFile, $response->getBody()->__toString());
-
- return $tmpZipFile;
- }
+ /**
+ * @param non-empty-string $localPath
+ *
+ * @return non-empty-string
+ */
+ public function downloadZipAndReturnLocalPath(RequestInterface $request, string $localPath): string;
}
diff --git a/src/Downloading/DownloadZipWithGuzzle.php b/src/Downloading/DownloadZipWithGuzzle.php
new file mode 100644
index 0000000..ab1873d
--- /dev/null
+++ b/src/Downloading/DownloadZipWithGuzzle.php
@@ -0,0 +1,45 @@
+client
+ ->sendAsync(
+ $request,
+ [
+ RequestOptions::ALLOW_REDIRECTS => true,
+ RequestOptions::HTTP_ERRORS => false,
+ RequestOptions::SYNCHRONOUS => true,
+ ],
+ )
+ ->wait();
+ assert($response instanceof ResponseInterface);
+
+ // @todo check response was successful
+
+ // @todo handle this writing better
+ $tmpZipFile = $localPath . '/downloaded.zip';
+ file_put_contents($tmpZipFile, $response->getBody()->__toString());
+
+ return $tmpZipFile;
+ }
+}
diff --git a/src/Downloading/Exception/CouldNotFindReleaseAsset.php b/src/Downloading/Exception/CouldNotFindReleaseAsset.php
new file mode 100644
index 0000000..ec1da61
--- /dev/null
+++ b/src/Downloading/Exception/CouldNotFindReleaseAsset.php
@@ -0,0 +1,22 @@
+prettyNameAndVersion(),
+ $expectedAssetName,
+ ));
+ }
+}
diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php
new file mode 100644
index 0000000..497d90d
--- /dev/null
+++ b/src/Downloading/GithubPackageReleaseAssets.php
@@ -0,0 +1,126 @@
+selectMatchingReleaseAsset(
+ $package,
+ $this->getReleaseAssetsForPackage($package),
+ );
+
+ return $releaseAsset['browser_download_url'];
+ }
+
+ /** @return non-empty-string */
+ private function expectedWindowsAssetName(Package $package): string
+ {
+ // @todo source these from the right places...
+ $arch = 'x86';
+ $ts = 'nts';
+ $compiler = 'vs16';
+ $phpVersion = '8.3';
+ $extensionName = str_replace('-', '_', 'example-pie-extension');
+
+ return sprintf(
+ 'php_%s-%s-%s-%s-%s-%s.zip',
+ $extensionName,
+ $package->version,
+ $phpVersion,
+ $compiler,
+ $ts,
+ $arch,
+ );
+ }
+
+ /** @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3734 */
+ // phpcs:disable Squiz.Commenting.FunctionComment.MissingParamName
+ /**
+ * @param list $releaseAssets
+ *
+ * @return array{name: non-empty-string, browser_download_url: non-empty-string, ...}
+ */
+ // phpcs:enable
+ private function selectMatchingReleaseAsset(Package $package, array $releaseAssets): array
+ {
+ $expectedAssetName = $this->expectedWindowsAssetName($package);
+
+ foreach ($releaseAssets as $releaseAsset) {
+ if ($releaseAsset['name'] === $expectedAssetName) {
+ return $releaseAsset;
+ }
+ }
+
+ throw Exception\CouldNotFindReleaseAsset::forPackage($package, $expectedAssetName);
+ }
+
+ /** @return list */
+ private function getReleaseAssetsForPackage(Package $package): array
+ {
+ // @todo dynamic URL, don't hard code it...
+ // @todo confirm prettyName will always match the repo name - it might not
+ $request = AddAuthenticationHeader::withAuthHeaderFromComposer(
+ new Request('GET', 'https://api.github.com/repos/' . $package->name . '/releases/tags/' . $package->version),
+ $package,
+ $this->authHelper,
+ );
+
+ $response = $this->client
+ ->sendAsync(
+ $request,
+ [
+ RequestOptions::ALLOW_REDIRECTS => true,
+ RequestOptions::HTTP_ERRORS => false,
+ RequestOptions::SYNCHRONOUS => true,
+ ],
+ )
+ ->wait();
+ assert($response instanceof ResponseInterface);
+
+ // @todo check response was successful
+
+ $releaseAssets = Json\typed(
+ (string) $response->getBody(),
+ Type\shape(
+ [
+ 'assets' => Type\vec(Type\shape(
+ [
+ 'name' => Type\non_empty_string(),
+ 'browser_download_url' => Type\non_empty_string(),
+ ],
+ true,
+ )),
+ ],
+ true,
+ ),
+ );
+
+ return $releaseAssets['assets'];
+ }
+}
diff --git a/src/Downloading/PackageReleaseAssets.php b/src/Downloading/PackageReleaseAssets.php
new file mode 100644
index 0000000..5872949
--- /dev/null
+++ b/src/Downloading/PackageReleaseAssets.php
@@ -0,0 +1,14 @@
+selectMatchingReleaseAsset(
- $package,
- $this->getReleaseAssetsForPackage($package),
- );
+ $windowsDownloadUrl = $this->packageReleaseAssets->findWindowsDownloadUrlForPackage($package);
// @todo extract to a static util
$localTempPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_downloader_', true);
@@ -50,7 +39,7 @@ public function __invoke(Package $package): DownloadedPackage
$tmpZipFile = $this->downloadZip->downloadZipAndReturnLocalPath(
AddAuthenticationHeader::withAuthHeaderFromComposer(
- new Request('GET', $releaseAsset['browser_download_url']),
+ new Request('GET', $windowsDownloadUrl),
$package,
$this->authHelper,
),
@@ -61,83 +50,4 @@ public function __invoke(Package $package): DownloadedPackage
return DownloadedPackage::fromPackageAndExtractedPath($package, $localTempPath);
}
-
- /** @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3734 */
- // phpcs:disable Squiz.Commenting.FunctionComment.MissingParamName
- /**
- * @param list $releaseAssets
- *
- * @return array{name: non-empty-string, browser_download_url: non-empty-string, ...}
- */
- // phpcs:enable
- private function selectMatchingReleaseAsset(Package $package, array $releaseAssets): array
- {
- // @todo source these from the right places...
- $arch = 'x86';
- $ts = 'nts';
- $compiler = 'vs16';
- $phpVersion = '8.3';
- $extensionName = str_replace('-', '_', 'example-pie-extension');
- $expectedAssetName = sprintf(
- 'php_%s-%s-%s-%s-%s-%s.zip',
- $extensionName,
- $package->version,
- $phpVersion,
- $compiler,
- $ts,
- $arch,
- );
-
- foreach ($releaseAssets as $releaseAsset) {
- if ($releaseAsset['name'] === $expectedAssetName) {
- return $releaseAsset;
- }
- }
-
- throw new RuntimeException('Could not find release asset for ' . $package->version . ' named: ' . $expectedAssetName);
- }
-
- /** @return list */
- private function getReleaseAssetsForPackage(Package $package): array
- {
- // @todo dynamic URL, don't hard code it...
- // @todo confirm prettyName will always match the repo name - it might not
- $request = AddAuthenticationHeader::withAuthHeaderFromComposer(
- new Request('GET', 'https://api.github.com/repos/' . $package->name . '/releases/tags/' . $package->version),
- $package,
- $this->authHelper,
- );
-
- $response = $this->client
- ->sendAsync(
- $request,
- [
- RequestOptions::ALLOW_REDIRECTS => true,
- RequestOptions::HTTP_ERRORS => false,
- RequestOptions::SYNCHRONOUS => true,
- ],
- )
- ->wait();
- assert($response instanceof ResponseInterface);
-
- // @todo check response was successful
-
- $releaseAssets = Json\typed(
- (string) $response->getBody(),
- Type\shape(
- [
- 'assets' => Type\vec(Type\shape(
- [
- 'name' => Type\non_empty_string(),
- 'browser_download_url' => Type\non_empty_string(),
- ],
- true,
- )),
- ],
- true,
- ),
- );
-
- return $releaseAssets['assets'];
- }
}
diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php
index def32e0..02ec7af 100644
--- a/test/integration/Command/DownloadCommandTest.php
+++ b/test/integration/Command/DownloadCommandTest.php
@@ -36,7 +36,7 @@ public function testDownloadCommand(): void
$this->commandTester->assertCommandIsSuccessful();
$outputString = $this->commandTester->getDisplay();
- self::assertStringContainsString('Found package: asgrim/example-pie-extension (version: 1.0.0)', $outputString);
+ self::assertStringContainsString('Found package: asgrim/example-pie-extension:1.0.0', $outputString);
self::assertStringContainsString('Dist download URL: https://api.github.com/repos/asgrim/example-pie-extension/zipball/', $outputString);
self::assertStringContainsString('Extracted asgrim/example-pie-extension:1.0.0 source', $outputString);
}
diff --git a/test/unit/DependencyResolver/PackageTest.php b/test/unit/DependencyResolver/PackageTest.php
index b052cff..a18ed83 100644
--- a/test/unit/DependencyResolver/PackageTest.php
+++ b/test/unit/DependencyResolver/PackageTest.php
@@ -20,6 +20,7 @@ public function testFromComposerCompletePackage(): void
self::assertSame('foo', $package->name);
self::assertSame('1.2.3', $package->version);
+ self::assertSame('foo:1.2.3', $package->prettyNameAndVersion());
self::assertNull($package->downloadUrl);
}
}
diff --git a/test/unit/Downloading/DownloadZipTest.php b/test/unit/Downloading/DownloadZipWithGuzzleTest.php
similarity index 86%
rename from test/unit/Downloading/DownloadZipTest.php
rename to test/unit/Downloading/DownloadZipWithGuzzleTest.php
index a7e0473..ac58dcb 100644
--- a/test/unit/Downloading/DownloadZipTest.php
+++ b/test/unit/Downloading/DownloadZipWithGuzzleTest.php
@@ -9,7 +9,7 @@
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
-use Php\Pie\Downloading\DownloadZip;
+use Php\Pie\Downloading\DownloadZipWithGuzzle;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
@@ -21,8 +21,8 @@
use const DIRECTORY_SEPARATOR;
-#[CoversClass(DownloadZip::class)]
-final class DownloadZipTest extends TestCase
+#[CoversClass(DownloadZipWithGuzzle::class)]
+final class DownloadZipWithGuzzleTest extends TestCase
{
public function testDownloadZipAndReturnLocalPath(): void
{
@@ -43,7 +43,7 @@ public function testDownloadZipAndReturnLocalPath(): void
$localPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_test_', true);
mkdir($localPath, 0777, true);
- $downloadedZipFile = (new DownloadZip($guzzleMockClient))
+ $downloadedZipFile = (new DownloadZipWithGuzzle($guzzleMockClient))
->downloadZipAndReturnLocalPath(
new Request('GET', 'http://test-uri/'),
$localPath,
diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php
new file mode 100644
index 0000000..f24da83
--- /dev/null
+++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php
@@ -0,0 +1,23 @@
+getMessage());
+ }
+}
diff --git a/test/unit/Downloading/ExtractZipTest.php b/test/unit/Downloading/ExtractZipTest.php
index f6d7149..cbd8a3a 100644
--- a/test/unit/Downloading/ExtractZipTest.php
+++ b/test/unit/Downloading/ExtractZipTest.php
@@ -18,7 +18,6 @@
use const DIRECTORY_SEPARATOR;
-/** @covers \Php\Pie\Downloading\ExtractZip */
#[CoversClass(ExtractZip::class)]
final class ExtractZipTest extends TestCase
{
diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php
new file mode 100644
index 0000000..82c9c23
--- /dev/null
+++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php
@@ -0,0 +1,75 @@
+createMock(AuthHelper::class);
+
+ $mockHandler = new MockHandler([
+ new Response(
+ 200,
+ [],
+ json_encode([
+ 'assets' => [
+ [
+ 'name' => 'php_example_pie_extension-1.2.3-8.3-vs16-nts-x86.zip',
+ 'browser_download_url' => 'actual_download_url',
+ ],
+ ],
+ ]),
+ ),
+ ]);
+
+ $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]);
+
+ $package = new Package('asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true));
+
+ $releaseAssets = new GithubPackageReleaseAssets($authHelper, $guzzleMockClient);
+
+ self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($package));
+ }
+
+ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotFound(): void
+ {
+ $authHelper = $this->createMock(AuthHelper::class);
+
+ $mockHandler = new MockHandler([
+ new Response(
+ 200,
+ [],
+ json_encode([
+ 'assets' => [],
+ ]),
+ ),
+ ]);
+
+ $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]);
+
+ $package = new Package('asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true));
+
+ $releaseAssets = new GithubPackageReleaseAssets($authHelper, $guzzleMockClient);
+
+ $this->expectException(CouldNotFindReleaseAsset::class);
+ $releaseAssets->findWindowsDownloadUrlForPackage($package);
+ }
+}
diff --git a/test/unit/Downloading/UnixDownloadAndExtractTest.php b/test/unit/Downloading/UnixDownloadAndExtractTest.php
new file mode 100644
index 0000000..1f3b240
--- /dev/null
+++ b/test/unit/Downloading/UnixDownloadAndExtractTest.php
@@ -0,0 +1,55 @@
+createMock(DownloadZip::class);
+ $extractZip = $this->createMock(ExtractZip::class);
+ $authHelper = $this->createMock(AuthHelper::class);
+ $unixDownloadAndExtract = new UnixDownloadAndExtract($downloadZip, $extractZip, $authHelper);
+
+ $tmpZipFile = uniqid('tmpZipFile', true);
+ $extractedPath = uniqid('extractedPath', true);
+
+ $downloadZip->expects(self::once())
+ ->method('downloadZipAndReturnLocalPath')
+ ->with(
+ self::isInstanceOf(RequestInterface::class),
+ self::isType('string'),
+ )
+ ->willReturn($tmpZipFile);
+
+ $extractZip->expects(self::once())
+ ->method('to')
+ ->with(
+ $tmpZipFile,
+ self::isType('string'),
+ )
+ ->willReturn($extractedPath);
+
+ $downloadUrl = 'https://test-uri/' . uniqid('downloadUrl', true);
+ $requestedPackage = new Package('foo/bar', '1.2.3', $downloadUrl);
+
+ $downloadedPackage = $unixDownloadAndExtract->__invoke($requestedPackage);
+
+ self::assertSame($requestedPackage, $downloadedPackage->package);
+ self::assertSame($extractedPath, $downloadedPackage->extractedSourcePath);
+ }
+}
diff --git a/test/unit/Downloading/WindowsDownloadAndExtractTest.php b/test/unit/Downloading/WindowsDownloadAndExtractTest.php
new file mode 100644
index 0000000..041dfa7
--- /dev/null
+++ b/test/unit/Downloading/WindowsDownloadAndExtractTest.php
@@ -0,0 +1,69 @@
+createMock(DownloadZip::class);
+ $extractZip = $this->createMock(ExtractZip::class);
+ $authHelper = $this->createMock(AuthHelper::class);
+ $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class);
+ $windowsDownloadAndExtract = new WindowsDownloadAndExtract(
+ $downloadZip,
+ $extractZip,
+ $authHelper,
+ $packageReleaseAssets,
+ );
+
+ $packageReleaseAssets->expects(self::once())
+ ->method('findWindowsDownloadUrlForPackage')
+ ->with(self::isInstanceOf(Package::class))
+ ->willReturn(uniqid('windowsDownloadUrl', true));
+
+ $tmpZipFile = uniqid('tmpZipFile', true);
+ $extractedPath = uniqid('extractedPath', true);
+
+ $downloadZip->expects(self::once())
+ ->method('downloadZipAndReturnLocalPath')
+ ->with(
+ self::isInstanceOf(RequestInterface::class),
+ self::isType('string'),
+ )
+ ->willReturn($tmpZipFile);
+
+ $extractZip->expects(self::once())
+ ->method('to')
+ ->with(
+ $tmpZipFile,
+ self::isType('string'),
+ )
+ ->willReturn($extractedPath);
+
+ $requestedPackage = new Package('foo/bar', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true));
+
+ $downloadedPackage = $windowsDownloadAndExtract->__invoke($requestedPackage);
+
+ self::assertSame($requestedPackage, $downloadedPackage->package);
+ self::assertStringContainsString(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'pie_downloader_', $downloadedPackage->extractedSourcePath);
+ }
+}