Skip to content

Commit

Permalink
Split out some components and add test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
asgrim committed Apr 24, 2024
1 parent c60458f commit 32cc5b6
Show file tree
Hide file tree
Showing 18 changed files with 471 additions and 166 deletions.
7 changes: 3 additions & 4 deletions src/Command/DownloadCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,14 @@ public function execute(InputInterface $input, OutputInterface $output): int
$requestedNameAndVersionPair['version'],
);

$output->writeln(sprintf('<info>Found package:</info> %s (version: %s)', $package->name, $package->version));
$output->writeln(sprintf('<info>Found package:</info> %s', $package->prettyNameAndVersion()));
$output->writeln(sprintf('<info>Dist download URL:</info> %s', $package->downloadUrl ?? '(none)'));

$downloadedPackage = ($this->downloadAndExtract)($package);

$output->writeln(sprintf(
'<info>Extracted %s:%s source to:</info> %s',
$downloadedPackage->package->name,
$downloadedPackage->package->version,
'<info>Extracted %s source to:</info> %s',
$downloadedPackage->package->prettyNameAndVersion(),
$downloadedPackage->extractedSourcePath,
));

Expand Down
44 changes: 16 additions & 28 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions src/DependencyResolver/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com
$completePackage->getDistUrl(),
);
}

public function prettyNameAndVersion(): string
{
return $this->name . ':' . $this->version;
}
}
41 changes: 7 additions & 34 deletions src/Downloading/DownloadZip.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
45 changes: 45 additions & 0 deletions src/Downloading/DownloadZipWithGuzzle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

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 DownloadZipWithGuzzle implements 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;
}
}
22 changes: 22 additions & 0 deletions src/Downloading/Exception/CouldNotFindReleaseAsset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Php\Pie\Downloading\Exception;

use Php\Pie\DependencyResolver\Package;
use RuntimeException;

use function sprintf;

class CouldNotFindReleaseAsset extends RuntimeException
{
public static function forPackage(Package $package, string $expectedAssetName): self
{
return new self(sprintf(
'Could not find release asset for %s named "%s"',
$package->prettyNameAndVersion(),
$expectedAssetName,
));
}
}
126 changes: 126 additions & 0 deletions src/Downloading/GithubPackageReleaseAssets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

declare(strict_types=1);

namespace Php\Pie\Downloading;

use Composer\Util\AuthHelper;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\RequestOptions;
use Php\Pie\DependencyResolver\Package;
use Psl\Json;
use Psl\Type;
use Psr\Http\Message\ResponseInterface;

use function assert;
use function sprintf;
use function str_replace;

/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
final class GithubPackageReleaseAssets implements PackageReleaseAssets
{
/** @psalm-api */
public function __construct(
private readonly AuthHelper $authHelper,
private readonly ClientInterface $client,
) {
}

/** @return non-empty-string */
public function findWindowsDownloadUrlForPackage(Package $package): string
{
$releaseAsset = $this->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<array{name: non-empty-string, browser_download_url: non-empty-string, ...}> $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<array{name: non-empty-string, browser_download_url: non-empty-string, ...}> */
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'];
}
}
14 changes: 14 additions & 0 deletions src/Downloading/PackageReleaseAssets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Php\Pie\Downloading;

use Php\Pie\DependencyResolver\Package;

/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
interface PackageReleaseAssets
{
/** @return non-empty-string */
public function findWindowsDownloadUrlForPackage(Package $package): string;
}
1 change: 1 addition & 0 deletions src/Downloading/UnixDownloadAndExtract.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
final class UnixDownloadAndExtract implements DownloadAndExtract
{
/** @psalm-api */
public function __construct(
private readonly DownloadZip $downloadZip,
private readonly ExtractZip $extractZip,
Expand Down
Loading

0 comments on commit 32cc5b6

Please sign in to comment.