├── .editorconfig ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Exceptions └── InvalidArgumentException.php ├── PackagistClient.php ├── PackagistUrlGenerator.php └── PackagistVendorFormatter.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `packagist-api` will be documented in this file 4 | 5 | ## 2.1.1 - 2025-03-21 6 | 7 | ### What's Changed 8 | 9 | * [B/C Break] Use the v2 metadata for package metadata, misc. dependency updates, new API endpoints by @mbabker in https://github.com/spatie/packagist-api/pull/22 10 | 11 | ### New Contributors 12 | 13 | * @mbabker made their first contribution in https://github.com/spatie/packagist-api/pull/22 14 | 15 | **Full Changelog**: https://github.com/spatie/packagist-api/compare/2.1.0...2.1.1 16 | 17 | ## 2.1.0 - 2022-08-01 18 | 19 | ### What's Changed 20 | 21 | - Add PHP 8.1 Support by @patinthehat in https://github.com/spatie/packagist-api/pull/20 22 | - Add security advisory endpoint by @GuySartorelli in https://github.com/spatie/packagist-api/pull/21 23 | 24 | ### New Contributors 25 | 26 | - @patinthehat made their first contribution in https://github.com/spatie/packagist-api/pull/20 27 | - @GuySartorelli made their first contribution in https://github.com/spatie/packagist-api/pull/21 28 | 29 | **Full Changelog**: https://github.com/spatie/packagist-api/compare/2.0.2...2.1.0 30 | 31 | ## 2.0.2 - 2020-12-01 32 | 33 | - add support for PHP 8 ([#19](https://github.com/spatie/packagist-api/pull/19)) 34 | 35 | ## 2.0.1 - 2020-09-10 36 | 37 | - allow Guzzle 7 38 | 39 | ## 2.0.0 - 2020-04-08 40 | 41 | - Renamed `Packagist` class to `PackagistClient`. 42 | - Changed the interface of the `PackagistClient` class to reflect the current state of the [Packagist API](https://packagist.org/apidoc) 43 | - Moved url generation to a separate class. 44 | - Moved vendor formatting to a separate class. 45 | - Changed the meta data method to actually use the repository endpoint. 46 | - Split up integration and unit tests 47 | 48 | ## 1.3.1 - 2020-03-25 49 | 50 | - add support for PHP 7.2 51 | 52 | ## 1.3.0 - 2020-03-25 53 | 54 | - Dropped support for anything below PHP 7.3 55 | 56 | ## 1.2.1 - 2019-04-10 57 | 58 | - throw error in find package name if string doesn't contain '/' 59 | 60 | ## 1.2.0 - 2018-08-29 61 | 62 | - add ability to search by both package and type 63 | 64 | ## 1.1.0 - 2018-05-08 65 | 66 | - add `findPackageByType` 67 | 68 | ## 1.0.1 - 2017-06-13 69 | 70 | - throw an exception when passing an empty value to `getPackagesByVendor` 71 | 72 | ## 1.0.0 - 2016-05-14 73 | 74 | - initial release 75 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fetch package info from Packagist 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/packagist-api.svg?style=flat-square)](https://packagist.org/packages/spatie/packagist-api) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 5 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/spatie/packagist-api/run-tests?label=tests) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/packagist-api.svg?style=flat-square)](https://packagist.org/packages/spatie/packagist-api) 7 | 8 | This package makes it easy to search and fetch package info using [the Packagist API](https://packagist.org/apidoc). 9 | 10 | ## Support us 11 | 12 | [](https://spatie.be/github-ad-click/packagist-api) 13 | 14 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 15 | 16 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 17 | 18 | ## Installation 19 | 20 | You can install the package via composer: 21 | 22 | ``` bash 23 | composer require spatie/packagist-api 24 | ``` 25 | 26 | There is also a [Laravel wrapper](https://packagist.org/packages/markwalet/laravel-packagist) available for this package. 27 | ## Usage 28 | 29 | You must pass a Guzzle client and a url generator to the constructor of `Spatie\Packagist\PackagistClient`. 30 | 31 | ```php 32 | $client = new \GuzzleHttp\Client(); 33 | $generator = new \Spatie\Packagist\PackagistUrlGenerator(); 34 | 35 | $packagist = new \Spatie\Packagist\PackagistClient($client, $generator); 36 | ``` 37 | 38 | ### List package names 39 | ```php 40 | // All packages 41 | $packagist->getPackagesNames(); 42 | 43 | // List packages by type. 44 | $packagist->getPackagesNamesByType('composer-plugin'); 45 | 46 | // List packages by organization 47 | $packagist->getPackagesNamesByVendor('spatie'); 48 | ``` 49 | 50 | ### List popular packages 51 | ```php 52 | // List first page of popular packages 53 | $packagist->getPopularPackages(); 54 | ``` 55 | 56 | #### Pagination 57 | Listing popular packages returns a paginated result. You can change the pagination settings by adding more parameters. 58 | 59 | ```php 60 | // Get the third page, 10 items per page. 61 | $packagist->getPopularPackages(3, 10); 62 | ``` 63 | 64 | ### Searching for packages 65 | ```php 66 | // Search packages by name. 67 | $packagist->searchPackagesByName('packagist'); 68 | 69 | // Search packages by tag. 70 | $packagist->searchPackagesByTags('psr-3'); 71 | 72 | // Search packages by type. 73 | $packagist->searchPackagesByType('composer-plugin'); 74 | 75 | // Combined search. 76 | $packagist->searchPackages('packagist', ['type' => 'library']); 77 | ``` 78 | 79 | #### Pagination 80 | Searching for packages returns a paginated result. You can change the pagination settings by adding more parameters. 81 | 82 | ```php 83 | // Get the third page, 10 items per page. 84 | $packagist->searchPackagesByName('packagist', 3, 10); 85 | ``` 86 | 87 | ### Getting package data. 88 | ```php 89 | // Using the Composer metadata. (faster, but less data) 90 | $packagist->getPackageMetadata('spatie/packagist-api'); 91 | $packagist->getPackageMetadata('spatie', 'packagist-api'); 92 | 93 | // Using the Composer metadata for dev branches. 94 | $packagist->getPackageMetadata('spatie', 'packagist-api', true); 95 | 96 | // Using the API. (slower, cached for 12 hours by Packagist. 97 | $packagist->getPackage('spatie/packagist-api'); 98 | $packagist->getPackage('spatie', 'packagist-api'); 99 | ``` 100 | 101 | ### Get package download stats 102 | ```php 103 | $packagist->getPackageDownloadStats('spatie/packagist-api'); 104 | $packagist->getPackageDownloadStats('spatie', 'packagist-api'); 105 | ``` 106 | 107 | ### Get Statistics 108 | ```php 109 | $packagist->getStatistics(); 110 | ``` 111 | 112 | ### Get security vulnerability advisories 113 | ```php 114 | // Get advisories for specific packages 115 | $packages = ['spatie/packagist-api']; 116 | $advisories = $packagist->getAdvisories($packages); 117 | 118 | // Get advisories for specific packages that were updated after some timestamp 119 | $packages = ['spatie/packagist-api']; 120 | $advisories = $packagist->getAdvisories($packages, strtotime('2 weeks ago')); 121 | 122 | // Get advisories only for specific versions of specific packages 123 | $packages = ['spatie/packagist-api' => '2.0.2']; 124 | $advisories = $packagist->getAdvisoriesAffectingVersions($packages); 125 | ``` 126 | 127 | ## Changelog 128 | 129 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 130 | 131 | ## Testing 132 | 133 | ``` bash 134 | composer test 135 | ``` 136 | 137 | ## Contributing 138 | 139 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 140 | 141 | ## Security 142 | 143 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 144 | 145 | ## Credits 146 | 147 | - [Freek Van der Herten](https://github.com/freekmurze) 148 | - [Jolita Grazyte](https://github.com/JolitaGrazyte) 149 | - [Mark Walet](https://github.com/markwalet) 150 | - [All Contributors](../../contributors) 151 | 152 | ## About Spatie 153 | Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). 154 | 155 | ## License 156 | 157 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 158 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/packagist-api", 3 | "description": "Fetch package info from Packagist", 4 | "keywords": [ 5 | "spatie", 6 | "packagist", 7 | "api" 8 | ], 9 | "homepage": "https://github.com/spatie/packagist-api", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Freek Van der Herten", 14 | "email": "freek@spatie.be", 15 | "homepage": "https://spatie.be", 16 | "role": "Developer" 17 | }, 18 | { 19 | "name": "Jolita Grazyte", 20 | "email": "jolita@spatie.be", 21 | "homepage": "https://spatie.be", 22 | "role": "Developer" 23 | }, 24 | { 25 | "name": "Mark Walet", 26 | "homepage": "https://markwalet.me", 27 | "role": "Developer" 28 | } 29 | ], 30 | "require": { 31 | "php": "^8.2", 32 | "ext-json": "*", 33 | "guzzlehttp/guzzle": "^7.0", 34 | "composer/semver": "^3.0" 35 | }, 36 | "require-dev": { 37 | "laravel/pint": "^1.17.3", 38 | "phpunit/phpunit": "^11.5 || ^12.0", 39 | "spatie/phpunit-snapshot-assertions": "^5.1" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "Spatie\\Packagist\\": "src" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "Spatie\\Packagist\\Test\\": "tests" 49 | } 50 | }, 51 | "scripts": { 52 | "test": "vendor/bin/phpunit", 53 | "lint": "pint" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | request('packages/list.json', array_filter(['type' => $type, 'vendor' => $vendor])); 19 | } 20 | 21 | public function getPackagesNamesByType(string $type): ?array 22 | { 23 | return $this->getPackagesNames($type, null); 24 | } 25 | 26 | public function getPackagesNamesByVendor(string $vendor): ?array 27 | { 28 | return $this->getPackagesNames(null, $vendor); 29 | } 30 | 31 | public function getPopularPackages(?int $page = 1, int $perPage = 100): ?array 32 | { 33 | $filters['page'] = $page; 34 | $filters['per_page'] = $perPage; 35 | 36 | return $this->request('explore/popular.json', $filters); 37 | } 38 | 39 | public function searchPackages($name = null, array $filters = [], ?int $page = 1, int $perPage = 15): ?array 40 | { 41 | if (count(array_diff(array_keys($filters), ['tags', 'type'])) > 0) { 42 | throw new InvalidArgumentException('Cannot search packages on this parameter. Allowed: `tags` & `type`.'); 43 | } 44 | 45 | $filters['q'] = $name; 46 | $filters['page'] = $page; 47 | $filters['per_page'] = $perPage; 48 | 49 | return $this->request('search.json', $filters); 50 | } 51 | 52 | public function searchPackagesByName(string $name, ?int $page = 1, int $perPage = 15): ?array 53 | { 54 | return $this->searchPackages($name, [], $page, $perPage); 55 | } 56 | 57 | public function searchPackagesByTags(string $tags, ?string $name = null, ?int $page = 1, int $perPage = 15): ?array 58 | { 59 | return $this->searchPackages($name, ['tags' => $tags], $page, $perPage); 60 | } 61 | 62 | public function searchPackagesByType(string $type, ?string $name = null, ?int $page = 1, int $perPage = 15): ?array 63 | { 64 | return $this->searchPackages($name, ['type' => $type], $page, $perPage); 65 | } 66 | 67 | public function getPackage(string $vendor, ?string $package = null): ?array 68 | { 69 | [$vendor, $package] = PackagistVendorFormatter::format($vendor, $package); 70 | $resource = 'packages/'.$vendor.'/'.$package.'.json'; 71 | 72 | return $this->request($resource, [], PackagistUrlGenerator::API_MODE); 73 | } 74 | 75 | public function getPackageMetadata(string $vendor, ?string $package = null, bool $devVersions = false): ?array 76 | { 77 | [$vendor, $package] = PackagistVendorFormatter::format($vendor, $package); 78 | $resource = 'p2/'.$vendor.'/'.$package.($devVersions ? '~dev' : '').'.json'; 79 | 80 | return $this->request($resource, [], PackagistUrlGenerator::REPO_MODE); 81 | } 82 | 83 | public function getPackageDownloadStats(string $vendor, ?string $package = null): ?array 84 | { 85 | [$vendor, $package] = PackagistVendorFormatter::format($vendor, $package); 86 | $resource = 'packages/'.$vendor.'/'.$package.'/stats.json'; 87 | 88 | return $this->request($resource, [], PackagistUrlGenerator::API_MODE); 89 | } 90 | 91 | public function getStatistics(): ?array 92 | { 93 | return $this->request('statistics.json'); 94 | } 95 | 96 | public function getAdvisories(array $packages = [], ?int $updatedSince = null): array 97 | { 98 | if (count($packages) === 0) { 99 | throw new InvalidArgumentException( 100 | 'At least one package must be passed in.' 101 | ); 102 | } 103 | 104 | $query = []; 105 | 106 | if ($updatedSince !== null) { 107 | $query['updatedSince'] = $updatedSince; 108 | } 109 | 110 | $options = [ 111 | 'query' => array_filter($query), 112 | ]; 113 | 114 | $content = ['packages' => []]; 115 | 116 | foreach ($packages as $package => $version) { 117 | if (is_numeric($package)) { 118 | $package = $version; 119 | } 120 | $content['packages'][] = $package; 121 | } 122 | $options['headers']['Content-type'] = 'application/x-www-form-urlencoded'; 123 | $options['body'] = http_build_query($content); 124 | 125 | $response = $this->postRequest('api/security-advisories/', $options); 126 | 127 | if ($response === null) { 128 | return []; 129 | } 130 | 131 | return $response['advisories']; 132 | } 133 | 134 | public function getAdvisoriesAffectingVersions(array $packages = [], ?int $updatedSince = null): array 135 | { 136 | $advisories = $this->getAdvisories($packages, $updatedSince); 137 | 138 | if (count($advisories) > 0) { 139 | return $this->filterAdvisories($advisories, $packages); 140 | } 141 | 142 | return $advisories; 143 | } 144 | 145 | protected function filterAdvisories(array $advisories, array $packages): array 146 | { 147 | $filteredAdvisories = []; 148 | foreach ($packages as $package => $version) { 149 | if (! is_string($package) || ! is_string($version)) { 150 | throw new InvalidArgumentException('$packages array must have package names as keys and versions as values.'); 151 | } 152 | 153 | if (array_key_exists($package, $advisories)) { 154 | foreach ($advisories[$package] as $advisory) { 155 | if (Semver::satisfies($version, $advisory['affectedVersions'])) { 156 | $filteredAdvisories[$package][] = $advisory; 157 | } 158 | } 159 | } 160 | } 161 | 162 | return $filteredAdvisories; 163 | } 164 | 165 | public function postRequest(string $resource, array $options = [], string $mode = PackagistUrlGenerator::API_MODE): ?array 166 | { 167 | $url = $this->url->make($resource, $mode); 168 | $response = $this->client 169 | ->post($url, $options) 170 | ->getBody() 171 | ->getContents(); 172 | 173 | return json_decode($response, true); 174 | } 175 | 176 | public function request(string $resource, array $query = [], string $mode = PackagistUrlGenerator::API_MODE): ?array 177 | { 178 | $url = $this->url->make($resource, $mode); 179 | $response = $this->client 180 | ->get($url, ['query' => array_filter($query)]) 181 | ->getBody() 182 | ->getContents(); 183 | 184 | return json_decode($response, true); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/PackagistUrlGenerator.php: -------------------------------------------------------------------------------- 1 | formatUrl($baseUrl ?? 'https://packagist.org'); 18 | $config[self::REPO_MODE] = $this->formatUrl($repoUrl ?? 'https://repo.packagist.org'); 19 | 20 | $this->config = $config; 21 | } 22 | 23 | public function make(string $resource = '', string $mode = self::API_MODE): string 24 | { 25 | if (in_array($mode, [self::API_MODE, self::REPO_MODE]) === false) { 26 | throw new InvalidArgumentException("Mode '{$mode}' is not supported. Use the constants of the `PackagistUrlGenerator` to decide which mode to use."); 27 | } 28 | 29 | return $this->config[$mode].$resource; 30 | } 31 | 32 | private function formatUrl(string $url): string 33 | { 34 | return preg_replace('/(?:\/)+$/u', '', $url).'/'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/PackagistVendorFormatter.php: -------------------------------------------------------------------------------- 1 |