-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ability to retrieve detailed package information from Packagist API
- Loading branch information
1 parent
01a67a9
commit 7838979
Showing
8 changed files
with
288 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
use Winter\Packager\Package\DetailedPackage; | ||
use Winter\Packager\Package\DetailedVersionedPackage; | ||
use Winter\Packager\Package\Package; | ||
use Winter\Packager\Package\Packagist; | ||
use Winter\Packager\Package\VersionedPackage; | ||
|
||
/** | ||
|
@@ -457,4 +458,17 @@ public static function newConstraint(mixed ...$arguments): Constraint | |
$class = static::$packageClasses['constraint']; | ||
return new $class(...$arguments); | ||
} | ||
|
||
/** | ||
* Set the user agent for the Packagist API requests. | ||
* | ||
* To comply with Packagist's requirements for use of their API, we require that agent names contain a name or | ||
* reference to the system being used, and a contact email address in the format of: | ||
* | ||
* `Name or Reference <[email protected]>` | ||
*/ | ||
public static function setAgent(string $agent): void | ||
{ | ||
Packagist::setAgent($agent); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?php | ||
|
||
namespace Winter\Packager\Exceptions; | ||
|
||
/** | ||
* Packagist exception. | ||
* | ||
* Handles an exception thrown when communicating with the Packagist API. | ||
* | ||
* @author Ben Thomson | ||
* @since 0.3.0 | ||
*/ | ||
class PackagistException extends PackagerException | ||
{ | ||
protected $message = 'Unable to connect to or retrieve a valid response from Packagist.'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,7 @@ | |
* @author Ben Thomson <[email protected]> | ||
* @since 0.3.0 | ||
*/ | ||
class DetailedVersionedPackage extends Package | ||
class DetailedVersionedPackage extends DetailedPackage | ||
{ | ||
protected string $versionNormalized; | ||
protected string $latestVersionNormalized; | ||
|
@@ -51,7 +51,25 @@ public function __construct( | |
protected string $latestVersion = '', | ||
protected VersionStatus $updateStatus = VersionStatus::UP_TO_DATE, | ||
) { | ||
parent::__construct($namespace, $name, $description); | ||
parent::__construct( | ||
$namespace, | ||
$name, | ||
$description, | ||
$type, | ||
$keywords, | ||
$homepage, | ||
$authors, | ||
$licenses, | ||
$support, | ||
$funding, | ||
$requires, | ||
$devRequires, | ||
$extras, | ||
$suggests, | ||
$conflicts, | ||
$replaces, | ||
$readme, | ||
); | ||
|
||
$this->versionNormalized = $this->normalizeVersion($version); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
<?php | ||
|
||
namespace Winter\Packager\Package; | ||
|
||
use Composer\MetadataMinifier\MetadataMinifier; | ||
use Composer\Semver\VersionParser; | ||
use Http\Discovery\Psr17FactoryDiscovery; | ||
use Http\Discovery\Psr18ClientDiscovery; | ||
use Psr\Http\Client\ClientInterface; | ||
use Psr\Http\Message\RequestInterface; | ||
use Winter\Packager\Exceptions\PackagistException; | ||
|
||
/** | ||
* Packagist class. | ||
* | ||
* Handles connecting to and making requests against the Packagist API. The Packagist API (generally) contains more | ||
* information about a package than Composer offers directly, thus we use it to augment the information retrieved from | ||
* Composer. | ||
* | ||
* @author Ben Thomson <[email protected]> | ||
* @since 0.3.0 | ||
*/ | ||
class Packagist | ||
{ | ||
protected const PACKAGIST_API_URL = 'https://packagist.org/'; | ||
protected const PACKAGIST_REPO_URL = 'https://repo.packagist.org/p2/'; | ||
|
||
protected static string $agent = 'Winter Packager <[email protected]>'; | ||
|
||
/** | ||
* Get information on a package in the Packagist API. | ||
* | ||
* @return array<string, mixed> | ||
*/ | ||
public static function getPackage(string $namespace, string $name, ?string $version = null): array | ||
{ | ||
$client = static::getClient(); | ||
$request = static::newRepoRequest($namespace . '/' . $name . '.json'); | ||
|
||
$response = $client->sendRequest($request); | ||
|
||
if ($response->getStatusCode() === 404) { | ||
throw new PackagistException('Package not found'); | ||
} | ||
|
||
if ($response->getStatusCode() !== 200) { | ||
throw new PackagistException('Failed to retrieve package information'); | ||
} | ||
|
||
$body = json_decode($response->getBody()->getContents(), true); | ||
|
||
if (is_null($version)) { | ||
if (!isset($body['packages'][$namespace . '/' . $name][0])) { | ||
throw new PackagistException('Package information not found'); | ||
} | ||
} else { | ||
if (!isset($body['packages'][$namespace . '/' . $name])) { | ||
throw new PackagistException('Package information not found'); | ||
} | ||
|
||
$versions = MetadataMinifier::expand($body['packages'][$namespace . '/' . $name]); | ||
$parser = new VersionParser; | ||
$packageVersionNormalized = $parser->normalize($version); | ||
|
||
foreach ($versions as $packageVersion) { | ||
if ($packageVersion['version_normalized'] === $packageVersionNormalized) { | ||
return $packageVersion; | ||
} | ||
} | ||
|
||
throw new PackagistException('Package version not found'); | ||
} | ||
|
||
return $body['packages'][$namespace . '/' . $name][0]; | ||
} | ||
|
||
public static function getClient(): ClientInterface | ||
{ | ||
return Psr18ClientDiscovery::find(); | ||
} | ||
|
||
/** | ||
* Set the user agent for the Packagist API requests. | ||
* | ||
* To comply with Packagist's requirements for use of their API, we require that agent names contain a name or | ||
* reference to the system being used, and a contact email address in the format of: | ||
* | ||
* `Name or Reference <[email protected]>` | ||
*/ | ||
public static function setAgent(string $agent): void | ||
{ | ||
if (!preg_match('/^(.+) <(.+)>$/', $agent, $matches)) { | ||
throw new \InvalidArgumentException( | ||
'Agent must be in the format of `Name or Reference <[email protected]>`' | ||
); | ||
} | ||
|
||
[$name, $email] = $matches; | ||
|
||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { | ||
throw new \InvalidArgumentException('Agent email address is not valid'); | ||
} | ||
|
||
static::$agent = trim($name) . ' <' . trim($email) . '>'; | ||
} | ||
|
||
public static function newApiRequest(string $url = ''): RequestInterface | ||
{ | ||
$request = Psr17FactoryDiscovery::findRequestFactory()->createRequest('GET', self::PACKAGIST_API_URL . ltrim($url, '/')); | ||
$request->withHeader('Accept', 'application/json'); | ||
$request->withHeader('Content-Type', 'application/json'); | ||
$request->withHeader('User-Agent', static::$agent); | ||
|
||
return $request; | ||
} | ||
|
||
public static function newRepoRequest(string $url = ''): RequestInterface | ||
{ | ||
$request = Psr17FactoryDiscovery::findRequestFactory()->createRequest('GET', self::PACKAGIST_REPO_URL . ltrim($url, '/')); | ||
$request->withHeader('Accept', 'application/json'); | ||
$request->withHeader('Content-Type', 'application/json'); | ||
$request->withHeader('User-Agent', static::$agent); | ||
|
||
return $request; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Winter\Packager\Tests\Cases\Package; | ||
|
||
use Winter\Packager\Composer; | ||
use Winter\Packager\Tests\ComposerTestCase; | ||
|
||
/** | ||
* @testdox The Package class | ||
* @coversDefaultClass \Winter\Packager\Package\Package | ||
*/ | ||
class PackageTest extends ComposerTestCase | ||
{ | ||
/** | ||
* @test | ||
* @testdox it can convert a package to a detailed package | ||
* @covers \Winter\Packager\Package\Package::toDetailed | ||
*/ | ||
public function itCanConvertAPackageToADetailedPackage() | ||
{ | ||
$package = Composer::newPackage('winter', 'wn-pages-plugin'); | ||
$package = $package->toDetailed(); | ||
|
||
$this->assertInstanceOf(\Winter\Packager\Package\DetailedPackage::class, $package); | ||
$this->assertEquals('winter', $package->getNamespace()); | ||
$this->assertEquals('wn-pages-plugin', $package->getName()); | ||
$this->assertEquals('winter-plugin', $package->getType()); | ||
$this->assertEquals('https://github.com/wintercms/wn-pages-plugin', $package->getHomepage()); | ||
$this->assertArrayHasKey('installer-name', $package->getExtras()); | ||
$this->assertArrayHasKey('winter', $package->getExtras()); | ||
$this->assertEquals('pages', $package->getExtras()['installer-name']); | ||
} | ||
|
||
/** | ||
* @test | ||
* @testdox it can convert a versioned package to a detailed versioned package | ||
* @covers \Winter\Packager\Package\VersionedPackage::toDetailed | ||
*/ | ||
public function itCanConvertAVersionedPackageToADetailedPackage() | ||
{ | ||
$package = Composer::newVersionedPackage('winter', 'wn-pages-plugin', '', 'v2.0.3'); | ||
$package = $package->toDetailed(); | ||
|
||
$this->assertInstanceOf(\Winter\Packager\Package\DetailedVersionedPackage::class, $package); | ||
$this->assertEquals('winter', $package->getNamespace()); | ||
$this->assertEquals('wn-pages-plugin', $package->getName()); | ||
$this->assertEquals('winter-plugin', $package->getType()); | ||
$this->assertEquals('https://github.com/wintercms/wn-pages-plugin', $package->getHomepage()); | ||
$this->assertArrayHasKey('installer-name', $package->getExtras()); | ||
$this->assertArrayNotHasKey('winter', $package->getExtras()); | ||
$this->assertEquals('pages', $package->getExtras()['installer-name']); | ||
$this->assertEquals('2.0.3.0', $package->getVersionNormalized()); | ||
} | ||
} |