├── .github └── workflows │ └── test.yml ├── LICENSE ├── composer.json └── src └── Packagist └── Api ├── Client.php ├── PackageNotFoundException.php └── Result ├── AbstractResult.php ├── Advisory.php ├── Advisory └── Source.php ├── Factory.php ├── Package.php ├── Package ├── Author.php ├── Dist.php ├── Downloads.php ├── Maintainer.php ├── Source.php └── Version.php └── Result.php /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests and linting 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3'] 12 | 13 | name: PHP ${{ matrix.php-versions }} 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set PHP version 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: ${{ matrix.php-versions }} 22 | 23 | - name: Check PHP version 24 | run: php -v 25 | 26 | - name: Validate composer.json and composer.lock 27 | run: composer validate --strict 28 | 29 | - name: Cache Composer packages 30 | id: composer-cache 31 | uses: actions/cache@v4 32 | with: 33 | path: vendor 34 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 35 | restore-keys: | 36 | ${{ runner.os }}-php- 37 | 38 | - name: Install dependencies 39 | run: composer install --prefer-dist --no-progress 40 | 41 | - name: Run linting 42 | run: composer run-script lint 43 | 44 | - name: Run tests 45 | run: composer run-script test 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2015 KNP Labs 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knplabs/packagist-api", 3 | "type": "library", 4 | "description": "Packagist API client.", 5 | "keywords": ["packagist", "api", "composer"], 6 | "homepage": "http://knplabs.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "KnpLabs Team", 11 | "homepage": "http://knplabs.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^7.4 || ^8.0", 16 | "guzzlehttp/guzzle": "^6.0 || ^7.0", 17 | "doctrine/inflector": "^1.0 || ^2.0", 18 | "ext-json": "*", 19 | "composer/metadata-minifier": "^1.0", 20 | "composer/semver": "^1.0|^2.0|^3.0" 21 | }, 22 | "require-dev": { 23 | "phpspec/phpspec": "^6.0 || ^7.0", 24 | "squizlabs/php_codesniffer": "^3.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Packagist\\Api\\": "src/Packagist/Api/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "spec\\Packagist\\Api\\": "spec/Packagist/Api/" 34 | } 35 | }, 36 | "extra": { 37 | "branch-alias": { 38 | "dev-master": "2.x-dev" 39 | } 40 | }, 41 | "scripts": { 42 | "lint": "vendor/bin/phpcs --standard=PSR12 src/", 43 | "test": "vendor/bin/phpspec run -f pretty" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Packagist/Api/Client.php: -------------------------------------------------------------------------------- 1 | httpClient = $httpClient; 47 | $this->resultFactory = $resultFactory; 48 | $this->packagistUrl = $packagistUrl; 49 | } 50 | 51 | /** 52 | * Search packages 53 | * 54 | * Available filters: 55 | * 56 | * * vendor: vendor of package (require or require-dev in composer.json) 57 | * * type: type of package (type in composer.json) 58 | * * tags: tags of package (keywords in composer.json) 59 | * 60 | * @param string $query Name of package 61 | * @param array $filters An array of filters 62 | * @param int $limit Pages to limit results (0 = all pages) 63 | * 64 | * @return array The results 65 | */ 66 | public function search(string $query, array $filters = [], int $limit = 0): array 67 | { 68 | $results = $response = []; 69 | $filters['q'] = $query; 70 | $url = '/search.json?' . http_build_query($filters); 71 | $response['next'] = $this->url($url); 72 | 73 | do { 74 | $response = $this->request($response['next']); 75 | $response = $this->parse((string) $response); 76 | $createResult = $this->create($response); 77 | if (!is_array($createResult)) { 78 | $createResult = [$createResult]; 79 | } 80 | $results = array_merge($results, $createResult); 81 | if (isset($response['next'])) { 82 | parse_str(parse_url($response['next'], PHP_URL_QUERY), $parse); 83 | } 84 | } while (isset($response['next']) && (0 === $limit || $parse['page'] <= $limit)); 85 | 86 | return $results; 87 | } 88 | 89 | /** 90 | * Retrieves full package details, generated dynamically by the Packagist API. 91 | * Consider using {@link getComposer()} instead to use the Packagist API 92 | * more efficiently if you don't need all the full metadata for a package. 93 | * 94 | * @param string $package Full qualified name ex : myname/mypackage 95 | * @return array|Package A package instance or array of packages 96 | */ 97 | public function get(string $package) 98 | { 99 | return $this->respond(sprintf($this->url('/packages/%s.json'), $package)); 100 | } 101 | 102 | /** 103 | * Similar to {@link get()}, but uses Composer metadata which is Packagist's preferred 104 | * way of retrieving details, since responses are cached efficiently as static files 105 | * by the Packagist service. The response lacks some metadata that is provided 106 | * by {@link get()}, see https://packagist.org/apidoc for details. 107 | * 108 | * Caution: Returns an array of packages, you need to select the correct one 109 | * from the indexed array. 110 | * 111 | * @since 1.6 112 | * @param string $package Full qualified name ex : myname/mypackage 113 | * @return array An array of packages, including the requested one, containing releases and dev branch versions 114 | */ 115 | public function getComposer(string $package): array 116 | { 117 | return $this->multiRespond( 118 | sprintf($this->url('/p2/%s.json'), $package), 119 | sprintf($this->url('/p2/%s~dev.json'), $package) 120 | ); 121 | } 122 | 123 | /** 124 | * @see https://packagist.org/apidoc#get-package-data 125 | * Contains only tagged releases 126 | * @param string $package Full qualified name ex : myname/mypackage 127 | * @return Package[] An array of packages, including the requested one. 128 | */ 129 | public function getComposerReleases(string $package): array 130 | { 131 | return $this->respond(sprintf($this->url('/p2/%s.json'), $package)); 132 | } 133 | 134 | /** 135 | * @see https://packagist.org/apidoc#get-package-data 136 | * Contains only dev branches 137 | * @param string $package Full qualified name ex : myname/mypackage 138 | * @return Package[] An array of packages, including the requested one. 139 | */ 140 | public function getComposerBranches(string $package): array 141 | { 142 | return $this->respond(sprintf($this->url('/p2/%s~dev.json'), $package)); 143 | } 144 | 145 | /** 146 | * Search packages 147 | * 148 | * Available filters: 149 | * 150 | * * vendor: vendor of package (require or require-dev in composer.json) 151 | * * type: type of package (type in composer.json) 152 | * * tags: tags of package (keywords in composer.json) 153 | * 154 | * @param array $filters An array of filters 155 | * @return array|Package The results, or single result 156 | */ 157 | public function all(array $filters = []) 158 | { 159 | $url = '/packages/list.json'; 160 | if ($filters) { 161 | $url .= '?' . http_build_query($filters); 162 | } 163 | 164 | return $this->respond($this->url($url)); 165 | } 166 | 167 | /** 168 | * Popular packages 169 | * 170 | * @param int $total 171 | * @return array The results 172 | */ 173 | public function popular(int $total): array 174 | { 175 | $results = $response = array(); 176 | $url = '/explore/popular.json?' . http_build_query(array('page' => 1)); 177 | $response['next'] = $this->url($url); 178 | 179 | do { 180 | $response = $this->request($response['next']); 181 | $response = $this->parse((string) $response); 182 | $createResult = $this->create($response); 183 | if (!is_array($createResult)) { 184 | $createResult = [$createResult]; 185 | } 186 | $results = array_merge($results, $createResult); 187 | } while (count($results) < $total && isset($response['next'])); 188 | 189 | return array_slice($results, 0, $total); 190 | } 191 | 192 | /** 193 | * Get a list of known security vulnerability advisories 194 | * 195 | * $packages can be a simple array of package names, or an array with package names 196 | * as keys and version strings as values. 197 | * 198 | * If $filterByVersion is true, any packages which are not accompanied by a version 199 | * number will be ignored. 200 | * 201 | * @param array $packages 202 | * @param integer|null $updatedSince A unix timestamp. 203 | * Only advisories updated after this date/time will be included 204 | * @param boolean $filterByVersion If true, only advisories which affect the version of packages in the 205 | * $packages array will be included 206 | * @return Advisory[] 207 | */ 208 | public function advisories(array $packages = [], ?int $updatedSince = null, bool $filterByVersion = false): array 209 | { 210 | if (count($packages) === 0 && $updatedSince === null) { 211 | throw new \InvalidArgumentException( 212 | 'At least one package or an $updatedSince timestamp must be passed in.' 213 | ); 214 | } 215 | 216 | if (count($packages) === 0 && $filterByVersion) { 217 | return []; 218 | } 219 | 220 | // Add updatedSince to query if passed in 221 | $query = []; 222 | if ($updatedSince !== null) { 223 | $query['updatedSince'] = $updatedSince; 224 | } 225 | $options = [ 226 | 'query' => array_filter($query), 227 | ]; 228 | 229 | // Add packages if appropriate 230 | if (count($packages) > 0) { 231 | $content = ['packages' => []]; 232 | foreach ($packages as $package => $version) { 233 | if (is_numeric($package)) { 234 | $package = $version; 235 | } 236 | $content['packages'][] = $package; 237 | } 238 | $options['headers']['Content-type'] = 'application/x-www-form-urlencoded'; 239 | $options['body'] = http_build_query($content); 240 | } 241 | 242 | // Get advisories from API 243 | /** @var Advisory[] $advisories */ 244 | $advisories = $this->respondPost($this->url('/api/security-advisories/'), $options); 245 | 246 | // Filter advisories if necessary 247 | if (count($advisories) > 0 && $filterByVersion) { 248 | return $this->filterAdvisories($advisories, $packages); 249 | } 250 | 251 | return $advisories; 252 | } 253 | 254 | /** 255 | * Filter the advisories array to only include any advisories that affect 256 | * the versions of packages in the $packages array 257 | * 258 | * @param Advisory[] $advisories 259 | * @param array $packages 260 | * @return Advisory[] Filtered advisories array 261 | */ 262 | private function filterAdvisories(array $advisories, array $packages): array 263 | { 264 | $filteredAdvisories = []; 265 | foreach ($packages as $package => $version) { 266 | // Skip any packages with no declared versions 267 | if (is_numeric($package)) { 268 | continue; 269 | } 270 | // Filter advisories by version 271 | if (array_key_exists($package, $advisories)) { 272 | foreach ($advisories[$package] as $advisory) { 273 | if (Semver::satisfies($version, $advisory->getAffectedVersions())) { 274 | $filteredAdvisories[$package][] = $advisory; 275 | } 276 | } 277 | } 278 | } 279 | return $filteredAdvisories; 280 | } 281 | 282 | /** 283 | * Assemble the packagist URL with the route 284 | * 285 | * @param string $route API Route that we want to achieve 286 | * @return string Fully qualified URL 287 | */ 288 | protected function url(string $route): string 289 | { 290 | if (preg_match('{^/p\d?/}', $route) && $this->packagistUrl === 'https://packagist.org') { 291 | return 'https://repo.packagist.org' . $route; 292 | } 293 | 294 | return $this->packagistUrl . $route; 295 | } 296 | 297 | /** 298 | * Execute the url request and parse the response 299 | * 300 | * @param string $url 301 | * @return array|Package 302 | */ 303 | protected function respond(string $url) 304 | { 305 | $response = $this->request($url); 306 | $response = $this->parse((string) $response); 307 | 308 | return $this->create($response); 309 | } 310 | 311 | /** 312 | * Execute the POST request and parse the response 313 | * 314 | * @param string $url 315 | * @param array $option 316 | * @return array|Package 317 | */ 318 | protected function respondPost(string $url, array $options) 319 | { 320 | $response = $this->postRequest($url, $options); 321 | $response = $this->parse($response); 322 | 323 | return $this->create($response); 324 | } 325 | 326 | /** 327 | * Execute two URLs request, parse and merge the responses by adding the versions from the second URL 328 | * into the versions from the first URL. 329 | * 330 | * @param string $url1 331 | * @param string $url2 332 | * @return array|Package 333 | */ 334 | protected function multiRespond(string $url1, string $url2) 335 | { 336 | $response1 = $this->request($url1); 337 | $response1 = $this->parse((string) $response1); 338 | 339 | $response2 = $this->request($url2); 340 | $response2 = $this->parse((string) $response2); 341 | 342 | foreach ($response1['packages'] as $name => $package1) { 343 | if (empty($response2['packages'][$name])) { 344 | continue; 345 | } 346 | $response1['packages'][$name] = [ 347 | ...$response1['packages'][$name], 348 | ...$response2['packages'][$name], 349 | ]; 350 | } 351 | 352 | return $this->create($response1); 353 | } 354 | 355 | /** 356 | * Execute the POST request 357 | * 358 | * @param string $url 359 | * @param array $options 360 | * @return string 361 | * @throws GuzzleException 362 | */ 363 | protected function postRequest(string $url, array $options): string 364 | { 365 | return $this->httpClient 366 | ->request('POST', $url, $options) 367 | ->getBody() 368 | ->getContents(); 369 | } 370 | 371 | /** 372 | * Execute the request URL 373 | * 374 | * @param string $url 375 | * @return string 376 | * @throws PackageNotFoundException 377 | * @throws GuzzleException 378 | */ 379 | protected function request(string $url): string 380 | { 381 | try { 382 | return $this->httpClient 383 | ->request('GET', $url) 384 | ->getBody() 385 | ->getContents(); 386 | } catch (GuzzleException $e) { 387 | if ($e->getCode() === 404) { 388 | throw new PackageNotFoundException('The requested package was not found.', 404); 389 | } 390 | throw $e; 391 | } 392 | } 393 | 394 | /** 395 | * Decode JSON 396 | * 397 | * @param string $data JSON string 398 | * 399 | * @return array JSON decode 400 | */ 401 | protected function parse(string $data): array 402 | { 403 | return json_decode($data, true) ?? []; 404 | } 405 | 406 | /** 407 | * Hydrate the knowing type depending on passed data 408 | * 409 | * @param array $data 410 | * @return array|Package 411 | */ 412 | protected function create(array $data) 413 | { 414 | return $this->resultFactory->create($data); 415 | } 416 | 417 | /** 418 | * Change the packagist URL 419 | * 420 | * @param string $packagistUrl URL 421 | * @return $this 422 | */ 423 | public function setPackagistUrl(string $packagistUrl): self 424 | { 425 | $this->packagistUrl = $packagistUrl; 426 | return $this; 427 | } 428 | 429 | /** 430 | * Return the actual packagist URL 431 | * 432 | * @return string URL 433 | */ 434 | public function getPackagistUrl(): string 435 | { 436 | return $this->packagistUrl; 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /src/Packagist/Api/PackageNotFoundException.php: -------------------------------------------------------------------------------- 1 | build(); 14 | foreach ($data as $key => $value) { 15 | $property = $inflector->camelize($key); 16 | if (null === $value && !$this->isNullable($property)) { 17 | continue; 18 | } 19 | if (!property_exists($this, $property)) { 20 | continue; 21 | } 22 | $this->$property = $value; 23 | } 24 | } 25 | 26 | private function isNullable(string $property): bool 27 | { 28 | if (PHP_MAJOR_VERSION < 8 || !property_exists($this, $property)) { 29 | return true; 30 | } 31 | 32 | $reflection = new \ReflectionClass($this); 33 | try { 34 | $reflectionProperty = $reflection->getProperty($property); 35 | } catch (\ReflectionException $exception) { 36 | return false; 37 | } 38 | 39 | return null === $reflectionProperty->getDefaultValue(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Packagist/Api/Result/Advisory.php: -------------------------------------------------------------------------------- 1 | advisoryId; 36 | } 37 | 38 | public function getPackageName(): string 39 | { 40 | return $this->packageName; 41 | } 42 | 43 | public function getRemoteId(): string 44 | { 45 | return $this->remoteId; 46 | } 47 | 48 | public function getTitle(): string 49 | { 50 | return $this->title; 51 | } 52 | 53 | public function getLink(): string 54 | { 55 | return $this->link; 56 | } 57 | 58 | public function getCve(): string 59 | { 60 | return $this->cve; 61 | } 62 | 63 | public function getAffectedVersions(): string 64 | { 65 | return $this->affectedVersions; 66 | } 67 | 68 | /** 69 | * @return Source[] 70 | */ 71 | public function getSources(): array 72 | { 73 | return $this->sources; 74 | } 75 | 76 | public function getReportedAt(): string 77 | { 78 | return $this->reportedAt; 79 | } 80 | 81 | public function getComposerRepository(): string 82 | { 83 | return $this->composerRepository; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Packagist/Api/Result/Advisory/Source.php: -------------------------------------------------------------------------------- 1 | name; 16 | } 17 | 18 | public function getRemoteId(): string 19 | { 20 | return $this->remoteId; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Packagist/Api/Result/Factory.php: -------------------------------------------------------------------------------- 1 | createSearchResults($data['results']); 36 | } 37 | if (isset($data['packages'])) { 38 | $packageOrResult = $data['packages'][array_key_first($data['packages'])]; 39 | if (isset($packageOrResult['name'])) { 40 | // Used for /explore/popular.json 41 | return $this->createSearchResults($data['packages']); 42 | } 43 | if (isset($data['minified']) && 'composer/2.0' === $data['minified']) { 44 | // Used for /p2/.json 45 | return $this->createComposer2PackagesResults($data['packages']); 46 | } 47 | // Used for /p/.json 48 | return $this->createComposerPackagesResults($data['packages']); 49 | } 50 | if (isset($data['package'])) { 51 | return $this->createPackageResults($data['package']); 52 | } 53 | if (isset($data['packageNames'])) { 54 | return $data['packageNames']; 55 | } 56 | if (isset($data['advisories'])) { 57 | return $this->createAdvisoryResults($data['advisories']); 58 | } 59 | 60 | throw new InvalidArgumentException('Invalid input data.'); 61 | } 62 | 63 | /** 64 | * Create a collection of \Packagist\Api\Result\Result 65 | * 66 | * @param array $results 67 | * 68 | * @return Result[] 69 | */ 70 | public function createSearchResults(array $results): array 71 | { 72 | $created = []; 73 | foreach ($results as $key => $result) { 74 | $created[$key] = $this->createResult(Result::class, $result); 75 | } 76 | return $created; 77 | } 78 | 79 | /** 80 | * @param array $packages 81 | * @return Package[] 82 | */ 83 | public function createComposerPackagesResults(array $packages): array 84 | { 85 | $created = []; 86 | 87 | foreach ($packages as $name => $package) { 88 | // Create an empty package, only contains versions 89 | $createdPackage = [ 90 | 'versions' => [], 91 | ]; 92 | foreach ($package as $branch => $version) { 93 | $createdPackage['versions'][$branch] = $version; 94 | } 95 | 96 | $created[$name] = $this->createPackageResults($createdPackage); 97 | } 98 | 99 | return $created; 100 | } 101 | 102 | /** 103 | * @param array $package 104 | * @return Package 105 | */ 106 | public function createPackageResults(array $package): Package 107 | { 108 | $package['description'] ??= ''; 109 | $package['github_stars'] ??= 0; 110 | $package['github_watchers'] ??= 0; 111 | $package['github_forks'] ??= 0; 112 | $package['suggesters'] ??= 0; 113 | $package['dependents'] ??= 0; 114 | $package['downloads'] ??= null; 115 | $package['favers'] ??= 0; 116 | 117 | if (isset($package['maintainers']) && $package['maintainers']) { 118 | foreach ($package['maintainers'] as $key => $maintainer) { 119 | $package['maintainers'][$key] = $this->createResult(Maintainer::class, $maintainer); 120 | } 121 | } 122 | 123 | if (isset($package['downloads']) && is_array($package['downloads'])) { 124 | $package['downloads'] = $this->createResult(Downloads::class, $package['downloads']); 125 | } 126 | 127 | foreach ($package['versions'] as $branch => $version) { 128 | if (empty($version['name']) && !empty($package['name'])) { 129 | $version['name'] = $package['name']; 130 | } 131 | 132 | if (isset($version['license'])) { 133 | $version['licenses'] = is_array($version['license']) ? $version['license'] : [$version['license']]; 134 | unset($version['license']); 135 | } 136 | 137 | // Cast some potentially null properties to empty strings 138 | $version['name'] ??= ''; 139 | $version['type'] ??= ''; 140 | 141 | if (isset($version['authors']) && $version['authors']) { 142 | foreach ($version['authors'] as $key => $author) { 143 | // Cast some potentially null properties to empty strings 144 | $author['name'] ??= ''; 145 | $author['email'] ??= ''; 146 | $author['homepage'] ??= ''; 147 | $version['authors'][$key] = $this->createResult(Author::class, $author); 148 | } 149 | } 150 | 151 | if ($version['source']) { 152 | $version['source'] = $this->createResult(PackageSource::class, $version['source']); 153 | } 154 | 155 | if (isset($version['dist']) && $version['dist']) { 156 | $version['dist'] = $this->createResult(Dist::class, $version['dist']); 157 | } 158 | 159 | $package['versions'][$branch] = $this->createResult(Version::class, $version); 160 | } 161 | 162 | $created = new Package(); 163 | $created->fromArray($package); 164 | 165 | return $created; 166 | } 167 | 168 | /** 169 | * @param array $advisories 170 | * @return Advisory[] 171 | */ 172 | public function createAdvisoryResults(array $advisories): array 173 | { 174 | $created = []; 175 | foreach ($advisories as $package => $advisories2) { 176 | foreach ($advisories2 as $advisory) { 177 | $advisory['advisoryId'] ??= ''; 178 | $advisory['packageName'] ??= ''; 179 | $advisory['remoteId'] ??= ''; 180 | $advisory['title'] ??= ''; 181 | $advisory['link'] ??= ''; 182 | $advisory['cve'] ??= ''; 183 | $advisory['affectedVersions'] ??= ''; 184 | $advisory['sources'] ??= []; 185 | $advisory['reportedAt'] ??= ''; 186 | $advisory['composerRepository'] ??= ''; 187 | foreach ($advisory['sources'] as $i => $source) { 188 | $source['name'] ??= ''; 189 | $source['remoteId'] ??= ''; 190 | $advisory['sources'][$i] = $this->createResult(AdvisorySource::class, $source); 191 | } 192 | $created[$package][] = $this->createResult(Advisory::class, $advisory); 193 | } 194 | } 195 | return $created; 196 | } 197 | 198 | /** 199 | * Dynamically create an AbstractResult of type $class and hydrate 200 | * 201 | * @param string $class DataObject class 202 | * @param array $data Array of data 203 | * @return AbstractResult $class hydrated 204 | */ 205 | protected function createResult(string $class, array $data): AbstractResult 206 | { 207 | $result = new $class(); 208 | if (!$result instanceof AbstractResult) { 209 | throw new InvalidArgumentException('Class must extend AbstractResult'); 210 | } 211 | $result->fromArray($data); 212 | 213 | return $result; 214 | } 215 | 216 | /** 217 | * @param array $packages 218 | * @return Package[] 219 | */ 220 | public function createComposer2PackagesResults(array $packages): array 221 | { 222 | $created = []; 223 | 224 | foreach ($packages as $name => $package) { 225 | $package = MetadataMinifier::expand($package); 226 | // Create an empty package, only contains versions 227 | $createdPackage = array( 228 | 'versions' => [], 229 | ); 230 | foreach ($package as $version) { 231 | $createdPackage['versions'][$version['version']] = $version; 232 | } 233 | 234 | $created[$name] = $this->createPackageResults($createdPackage); 235 | } 236 | 237 | return $created; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/Packagist/Api/Result/Package.php: -------------------------------------------------------------------------------- 1 | name; 51 | } 52 | 53 | public function getDescription(): string 54 | { 55 | return $this->description; 56 | } 57 | 58 | public function getTime(): string 59 | { 60 | return $this->time; 61 | } 62 | 63 | /** 64 | * @return Package\Maintainer[] 65 | */ 66 | public function getMaintainers(): array 67 | { 68 | return $this->maintainers; 69 | } 70 | 71 | /** 72 | * @return Package\Version[] 73 | */ 74 | public function getVersions(): array 75 | { 76 | return $this->versions; 77 | } 78 | 79 | public function getType(): string 80 | { 81 | return $this->type; 82 | } 83 | 84 | public function getRepository(): string 85 | { 86 | return $this->repository; 87 | } 88 | 89 | public function getDownloads(): ?Downloads 90 | { 91 | return $this->downloads; 92 | } 93 | 94 | public function getFavers(): int 95 | { 96 | return $this->favers; 97 | } 98 | 99 | public function isAbandoned(): bool 100 | { 101 | return (bool) $this->abandoned; 102 | } 103 | 104 | /** 105 | * Gets the package name to use as a replacement if this package is abandoned 106 | * 107 | * @return string|null 108 | */ 109 | public function getReplacementPackage(): ?string 110 | { 111 | // The Packagist API will either return a boolean, or a string value for `abandoned`. It will be a boolean 112 | // if no replacement package was provided when the package was marked as abandoned in Packagist, or it will be 113 | // a string containing the replacement package name to use if one was provided. 114 | // @see https://github.com/KnpLabs/packagist-api/pull/56#discussion_r306426997 115 | if (is_string($this->abandoned)) { 116 | return $this->abandoned; 117 | } 118 | 119 | return null; 120 | } 121 | 122 | public function getSuggesters(): int 123 | { 124 | return $this->suggesters; 125 | } 126 | 127 | public function getDependents(): int 128 | { 129 | return $this->dependents; 130 | } 131 | 132 | public function getGithubStars(): int 133 | { 134 | return $this->githubStars; 135 | } 136 | 137 | public function getGithubForks(): int 138 | { 139 | return $this->githubForks; 140 | } 141 | 142 | public function getGithubWatchers(): int 143 | { 144 | return $this->githubWatchers; 145 | } 146 | 147 | public function getGithubOpenIssues(): int 148 | { 149 | return $this->githubOpenIssues; 150 | } 151 | 152 | public function getLanguage(): string 153 | { 154 | return $this->language; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Packagist/Api/Result/Package/Author.php: -------------------------------------------------------------------------------- 1 | role; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Packagist/Api/Result/Package/Dist.php: -------------------------------------------------------------------------------- 1 | shasum; 22 | } 23 | 24 | public function getType(): string 25 | { 26 | return $this->type; 27 | } 28 | 29 | public function getUrl(): string 30 | { 31 | return $this->url; 32 | } 33 | 34 | public function getReference(): ?string 35 | { 36 | return $this->reference; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Packagist/Api/Result/Package/Downloads.php: -------------------------------------------------------------------------------- 1 | total = $total; 20 | return $this; 21 | } 22 | 23 | public function setMonthly(int $monthly): self 24 | { 25 | $this->monthly = $monthly; 26 | return $this; 27 | } 28 | 29 | public function setDaily(int $daily): self 30 | { 31 | $this->daily = $daily; 32 | return $this; 33 | } 34 | 35 | public function getTotal(): int 36 | { 37 | return $this->total; 38 | } 39 | 40 | public function getMonthly(): int 41 | { 42 | return $this->monthly; 43 | } 44 | 45 | public function getDaily(): int 46 | { 47 | return $this->daily; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Packagist/Api/Result/Package/Maintainer.php: -------------------------------------------------------------------------------- 1 | name; 22 | } 23 | 24 | public function getEmail(): string 25 | { 26 | return $this->email; 27 | } 28 | 29 | public function getHomepage(): string 30 | { 31 | return $this->homepage; 32 | } 33 | 34 | public function getAvatarUrl(): string 35 | { 36 | return $this->avatarUrl; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Packagist/Api/Result/Package/Source.php: -------------------------------------------------------------------------------- 1 | type; 20 | } 21 | 22 | public function getUrl(): string 23 | { 24 | return $this->url; 25 | } 26 | 27 | public function getReference(): string 28 | { 29 | return $this->reference; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Packagist/Api/Result/Package/Version.php: -------------------------------------------------------------------------------- 1 | name; 67 | } 68 | 69 | public function getDescription(): string 70 | { 71 | return $this->description; 72 | } 73 | 74 | public function getKeywords(): array 75 | { 76 | return $this->keywords; 77 | } 78 | 79 | public function getHomepage(): string 80 | { 81 | return $this->homepage; 82 | } 83 | 84 | public function getVersion(): string 85 | { 86 | return $this->version; 87 | } 88 | 89 | public function getVersionNormalized(): string 90 | { 91 | return $this->versionNormalized; 92 | } 93 | 94 | public function getLicenses(): array 95 | { 96 | return $this->licenses; 97 | } 98 | 99 | public function getAuthors(): array 100 | { 101 | return $this->authors; 102 | } 103 | 104 | public function getSource(): ?Source 105 | { 106 | return $this->source; 107 | } 108 | 109 | public function getDist(): ?Dist 110 | { 111 | return $this->dist; 112 | } 113 | 114 | public function getType(): string 115 | { 116 | return $this->type; 117 | } 118 | 119 | public function getTime(): string 120 | { 121 | return $this->time; 122 | } 123 | 124 | public function getAutoload(): array 125 | { 126 | return $this->autoload; 127 | } 128 | 129 | public function getExtra(): array 130 | { 131 | return $this->extra; 132 | } 133 | 134 | public function getRequire(): array 135 | { 136 | return $this->require; 137 | } 138 | 139 | public function getRequireDev(): array 140 | { 141 | return $this->requireDev; 142 | } 143 | 144 | public function getConflict(): array 145 | { 146 | return $this->conflict; 147 | } 148 | 149 | public function getProvide(): array 150 | { 151 | return $this->provide; 152 | } 153 | 154 | public function getReplace(): array 155 | { 156 | return $this->replace; 157 | } 158 | 159 | public function getBin(): array 160 | { 161 | return $this->bin; 162 | } 163 | 164 | public function getSuggest(): array 165 | { 166 | return $this->suggest; 167 | } 168 | 169 | public function getSupport(): array 170 | { 171 | return $this->support; 172 | } 173 | 174 | public function getTargetDir(): string 175 | { 176 | return $this->targetDir; 177 | } 178 | 179 | public function getDefaultBranch(): bool 180 | { 181 | return $this->defaultBranch; 182 | } 183 | 184 | public function isAbandoned(): bool 185 | { 186 | return (bool) $this->abandoned; 187 | } 188 | 189 | /** 190 | * Gets the package name to use as a replacement if this package is abandoned 191 | * 192 | * @return string|null 193 | */ 194 | public function getReplacementPackage(): ?string 195 | { 196 | // The Packagist API will either return a boolean, or a string value for `abandoned`. It will be a boolean 197 | // if no replacement package was provided when the package was marked as abandoned in Packagist, or it will be 198 | // a string containing the replacement package name to use if one was provided. 199 | // @see https://github.com/KnpLabs/packagist-api/pull/56#discussion_r306426997 200 | if (is_string($this->abandoned)) { 201 | return $this->abandoned; 202 | } 203 | 204 | return null; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Packagist/Api/Result/Result.php: -------------------------------------------------------------------------------- 1 | name; 24 | } 25 | 26 | public function getDescription(): string 27 | { 28 | return $this->description; 29 | } 30 | 31 | public function getUrl(): string 32 | { 33 | return $this->url; 34 | } 35 | 36 | public function getDownloads(): int 37 | { 38 | return $this->downloads; 39 | } 40 | 41 | public function getFavers(): int 42 | { 43 | return $this->favers; 44 | } 45 | 46 | public function getRepository(): string 47 | { 48 | return $this->repository; 49 | } 50 | } 51 | --------------------------------------------------------------------------------