From f38109569b2280154016dc95f6d85f7b35d036bf Mon Sep 17 00:00:00 2001 From: Eric Sizemore Date: Fri, 15 Mar 2024 07:13:01 -0400 Subject: [PATCH] 2.0.0-dev --- .php-cs-fixer.dist.php | 11 + CHANGELOG.md | 66 +- LICENSE.md | 2 +- README.md | 10 +- SECURITY.md | 1 + composer.json | 3 +- composer.lock | 609 ++++++++---------- docs/basic-usage.md | 14 +- docs/changelog.md | 36 +- docs/index.md | 7 +- docs/installation.md | 6 +- docs/license.md | 2 +- docs/platform.md | 23 +- docs/project.md | 25 +- docs/repository.md | 22 +- docs/security.md | 1 + docs/subscription.md | 22 +- docs/user.md | 22 +- mkdocs.yml | 12 +- src/AbstractClient.php | 151 +++++ src/Exception/InvalidApiKeyException.php | 21 + src/Exception/InvalidEndpointException.php | 21 + .../InvalidEndpointOptionsException.php | 21 + src/Exception/RateLimitExceededException.php | 61 +- src/LibrariesIO.php | 498 ++------------ src/Utils.php | 225 +++++++ tests/src/LibrariesIOTest.php | 481 +++++++------- 27 files changed, 1256 insertions(+), 1117 deletions(-) create mode 100644 src/AbstractClient.php create mode 100644 src/Exception/InvalidApiKeyException.php create mode 100644 src/Exception/InvalidEndpointException.php create mode 100644 src/Exception/InvalidEndpointOptionsException.php create mode 100644 src/Utils.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 0548c34..7dc03f6 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -2,6 +2,15 @@ declare(strict_types=1); +$header = <<<'EOF' + This file is part of Esi\LibrariesIO. + + (c) 2023-2024 Eric Sizemore + + For the full copyright and license information, please view + the LICENSE file that was distributed with this source code. + EOF; + $config = new PhpCsFixer\Config(); $config ->setRiskyAllowed(true) @@ -53,6 +62,8 @@ 'declare_parentheses' => true, 'declare_strict_types' => true, //'global_namespace_import' => ['import_classes' => true, 'import_constants' => true, 'import_functions' => true], + 'header_comment' => ['comment_type' => 'PHPDoc', 'header' => $header, 'separate' => 'top'], + 'ordered_class_elements' => ['order' => ['use_trait', 'case', 'constant_public', 'constant_protected', 'constant_private', 'property_public', 'property_protected', 'property_private', 'construct', 'destruct', 'magic', 'phpunit', 'method_public', 'method_protected', 'method_private'], 'sort_algorithm' => 'alpha'], ]) ->setLineEnding("\n") ->setFinder( diff --git a/CHANGELOG.md b/CHANGELOG.md index 78239b5..6823fdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,64 @@ -## CHANGELOG +# CHANGELOG A not so exhaustive list of changes for each release. For a more detailed listing of changes between each version, -you can use the following url: https://github.com/ericsizemore/librariesio/compare/v1.0.0...v1.1.1. +you can use the following url: https://github.com/ericsizemore/librariesio/compare/v1.0.0...v2.0.0. Simply replace the version numbers depending on which set of changes you wish to see. -### 1.1.1 (2024-03-14) - * Added PHP-CS-Fixer to dev dependencies. +## 2.0.0-dev (TBD, ~2024-04-01) + +**NOTE:** Version 2.0.0 *should* be backwards compatible (for the most part) with 1.x. +However, if you make use of the `raw`, `toArray`, or `toObject` functions, please check the **CHANGED** section below. + +### Changed + + * The `LibrariesIO` constructor now takes a new option `$clientOptions` which is an array of options to pass to the Guzzle Client. + * Note, the class will ignore 'base_uri', 'http_errors', 'handler' and 'query' if passed into `$clientOptions` + * Changed unit tests to pass a `MockHandler` instance to the class (via `$clientOptions`), which is handled in the `AbstractClient` constructor, to ease mocking and testing. + * The `$client` property is now private. + * `raw`, `toArray`, `toObject` are no longer part of the main `LibrariesIO` class. + * They are instead in the `Utils` class and can be accessed statically: + * `Utils::raw()`, `Utils::toArray()`, `Utils::toObject()` + * The `LibrariesIO` class now only defines functions to access the API endpoints and leaves the rest of the work up to `AbstractClient` and `Utils`. + * `Exception\RateLimitExceededException` now takes `GuzzleHttp\Exception\ClientException` as a parameter. + +### Added + + * `AbstractClient` class which the main `LibrariesIO` class extends. + * `Utils` class which holds various useful static methods used throughout the library. + * New exceptions: + * Exception\InvalidApiKeyException + * Exception\InvalidEndpointException + * Exception\InvalidEndpointOptionsException + +### Removed + + * Removed the `$cachePath` property. + * Removed unnecessary comments throughout. + + +## 1.1.1 (2024-03-14) + +### Changed + * Updated/refactored some code to reduce duplicate checks/etc. throughout. * CS improvements/fixes. + +### Added + + * Added PHP-CS-Fixer to dev dependencies. + +### Removed + * Cleaned up some doc blocks and removed some unnecessary comments. -### 1.1.0 (2023-12-29) - * Added `subscription()` to handle adding, updating, checking and removing a subscription to a project. +## 1.1.0 (2023-12-29) + +### Changed + * Updated `makeRequest()` with a `$method` parameter to handle post, put, and delete requests in addition to get. * Visibility changed to `protected` for: * endpointParameters() @@ -24,6 +67,15 @@ Simply replace the version numbers depending on which set of changes you wish to * Converted line endings to linux, some files snuck through with Windows line endings * Documentation updated -### 1.0.0 (2023-12-25) +### Added + + * Added `subscription()` to handle adding, updating, checking and removing a subscription to a project. + +### Removed + + * None + + +## 1.0.0 (2023-12-25) * Initial release diff --git a/LICENSE.md b/LICENSE.md index 81bc9fc..f9d6301 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -3,7 +3,7 @@ The MIT License (MIT) The MIT License (MIT) -Copyright (c) 2023 Eric Sizemore +Copyright (c) 2023-2024 Eric Sizemore Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c88a4cb..90200b0 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,13 @@ [![Downloads per Month](https://img.shields.io/packagist/dm/esi/librariesio.svg)](https://packagist.org/packages/esi/librariesio) [![License](https://img.shields.io/packagist/l/esi/librariesio.svg)](https://packagist.org/packages/esi/librariesio) +## 2.0.0 Important Note + +* The `master` branch is for development of the upcoming version 2.0.0. +* Should be okay, but would still advise caution using in production. +* Function parameters, class api's, etc. may change during development. +* The [docs](docs) have not yet been updated with changes. + ## Important Note This project was born from the desire to expand my knowledge of API's and GuzzleHttp. My implementation is far from perfect, so I am open to any and all feedback that one may wish to provide. @@ -87,11 +94,12 @@ As an example, let's say you want to get a list of the available platforms. To d platform(); -print_r($api->toArray($response)); +print_r(Utils::toArray($response)); /* Array diff --git a/SECURITY.md b/SECURITY.md index 7848d03..3395b7e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,6 +4,7 @@ | Version | Supported | |---------|--------------------| +| 2.0.x | :white_check_mark: | | 1.1.x | :white_check_mark: | | 1.0.x | :white_check_mark: | diff --git a/composer.json b/composer.json index 0055063..82d2987 100644 --- a/composer.json +++ b/composer.json @@ -35,9 +35,10 @@ "phpstan/phpstan": "^1.11 <2.0", "phpstan/phpstan-phpunit": "^1.4", "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "^11" + "phpunit/phpunit": "^11.0" }, "minimum-stability": "dev", + "prefer-stable": true, "autoload": { "psr-4": { "Esi\\LibrariesIO\\": "src/" diff --git a/composer.lock b/composer.lock index 9711f4e..ccd3df7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,11 +4,11 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "185308559b0ce9297b2be9d5af3af615", + "content-hash": "de984b56e7b7443543bf9120f716bfc7", "packages": [ { "name": "guzzlehttp/guzzle", - "version": "7.9.x-dev", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", @@ -134,7 +134,7 @@ }, { "name": "guzzlehttp/promises", - "version": "2.0.x-dev", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", @@ -153,7 +153,6 @@ "bamarni/composer-bin-plugin": "^1.8.2", "phpunit/phpunit": "^8.5.36 || ^9.6.15" }, - "default-branch": true, "type": "library", "extra": { "bamarni-bin": { @@ -218,7 +217,7 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.6.x-dev", + "version": "2.6.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", @@ -248,7 +247,6 @@ "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, - "default-branch": true, "type": "library", "extra": { "bamarni-bin": { @@ -420,29 +418,25 @@ }, { "name": "psr/cache", - "version": "dev-master", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/cache.git", - "reference": "0a7c67d0d1c8167b342eb74339d6f961663826ce" + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/0a7c67d0d1c8167b342eb74339d6f961663826ce", - "reference": "0a7c67d0d1c8167b342eb74339d6f961663826ce", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", "shasum": "" }, "require": { "php": ">=8.0.0" }, - "suggest": { - "fig/cache-util": "Provides some useful PSR-6 utilities" - }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { @@ -467,28 +461,27 @@ "psr-6" ], "support": { - "source": "https://github.com/php-fig/cache/tree/master" + "source": "https://github.com/php-fig/cache/tree/3.0.0" }, - "time": "2021-02-24T03:25:37+00:00" + "time": "2021-02-03T23:26:27+00:00" }, { "name": "psr/container", - "version": "dev-master", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "707984727bd5b2b670e59559d3ed2500240cf875" + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/707984727bd5b2b670e59559d3ed2500240cf875", - "reference": "707984727bd5b2b670e59559d3ed2500240cf875", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { "php": ">=7.4.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -521,13 +514,13 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container" + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2023-09-22T11:11:30+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { "name": "psr/http-client", - "version": "dev-master", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", @@ -543,7 +536,6 @@ "php": "^7.0 || ^8.0", "psr/http-message": "^1.0 || ^2.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -580,23 +572,22 @@ }, { "name": "psr/http-factory", - "version": "dev-master", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "7037f4b0950474e9d1350e8df89b15f1842085f6" + "reference": "e616d01114759c4c489f93b099585439f795fe35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/7037f4b0950474e9d1350e8df89b15f1842085f6", - "reference": "7037f4b0950474e9d1350e8df89b15f1842085f6", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", "shasum": "" }, "require": { "php": ">=7.0.0", "psr/http-message": "^1.0 || ^2.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -618,7 +609,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "description": "Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -630,13 +621,13 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory" + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" }, - "time": "2023-09-22T11:16:44+00:00" + "time": "2023-04-10T20:10:41+00:00" }, { "name": "psr/http-message", - "version": "dev-master", + "version": "2.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", @@ -651,7 +642,6 @@ "require": { "php": "^7.2 || ^8.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -690,7 +680,7 @@ }, { "name": "psr/log", - "version": "dev-master", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", @@ -705,7 +695,6 @@ "require": { "php": ">=8.0.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -785,16 +774,16 @@ }, { "name": "symfony/cache", - "version": "7.1.x-dev", + "version": "v7.0.4", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "e43a80cb3c939ce78da9a58044cbbab1537aa6fa" + "reference": "fc822951dd360a593224bb2cef90a087d0dff60f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/e43a80cb3c939ce78da9a58044cbbab1537aa6fa", - "reference": "e43a80cb3c939ce78da9a58044cbbab1537aa6fa", + "url": "https://api.github.com/repos/symfony/cache/zipball/fc822951dd360a593224bb2cef90a087d0dff60f", + "reference": "fc822951dd360a593224bb2cef90a087d0dff60f", "shasum": "" }, "require": { @@ -802,7 +791,6 @@ "psr/cache": "^2.0|^3.0", "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^2.5|^3", - "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/service-contracts": "^2.5|^3", "symfony/var-exporter": "^6.4|^7.0" }, @@ -862,7 +850,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/7.1" + "source": "https://github.com/symfony/cache/tree/v7.0.4" }, "funding": [ { @@ -878,31 +866,30 @@ "type": "tidelift" } ], - "time": "2024-02-22T20:28:06+00:00" + "time": "2024-02-22T20:27:20+00:00" }, { "name": "symfony/cache-contracts", - "version": "dev-main", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "a9fe419f4bf8032ae4ed48e89a1c3d1dd04908be" + "reference": "1d74b127da04ffa87aa940abe15446fa89653778" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/a9fe419f4bf8032ae4ed48e89a1c3d1dd04908be", - "reference": "a9fe419f4bf8032ae4ed48e89a1c3d1dd04908be", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/1d74b127da04ffa87aa940abe15446fa89653778", + "reference": "1d74b127da04ffa87aa940abe15446fa89653778", "shasum": "" }, "require": { "php": ">=8.1", "psr/cache": "^3.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -939,7 +926,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/main" + "source": "https://github.com/symfony/cache-contracts/tree/v3.4.0" }, "funding": [ { @@ -955,30 +942,29 @@ "type": "tidelift" } ], - "time": "2024-01-23T15:06:13+00:00" + "time": "2023-09-25T12:52:38+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "dev-main", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "2c438b99bb2753c1628c1e6f523991edea5b03a4" + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/2c438b99bb2753c1628c1e6f523991edea5b03a4", - "reference": "2c438b99bb2753c1628c1e6f523991edea5b03a4", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "shasum": "" }, "require": { "php": ">=8.1" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -1007,7 +993,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/main" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" }, "funding": [ { @@ -1023,20 +1009,20 @@ "type": "tidelift" } ], - "time": "2024-01-02T14:07:37+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/service-contracts", - "version": "dev-main", + "version": "v3.4.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "cea2eccfcd27ac3deb252bd67f78b9b8ffc4da84" + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/cea2eccfcd27ac3deb252bd67f78b9b8ffc4da84", - "reference": "cea2eccfcd27ac3deb252bd67f78b9b8ffc4da84", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", "shasum": "" }, "require": { @@ -1046,11 +1032,10 @@ "conflict": { "ext-psr": "<1.1|>=2" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -1090,7 +1075,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/main" + "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" }, "funding": [ { @@ -1106,20 +1091,20 @@ "type": "tidelift" } ], - "time": "2024-01-02T14:07:37+00:00" + "time": "2023-12-26T14:02:43+00:00" }, { "name": "symfony/var-exporter", - "version": "7.1.x-dev", + "version": "v7.0.4", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "9ae44947281eb16213e3db6a280916af9e504f46" + "reference": "dfb0acb6803eb714f05d97dd4c5abe6d5fa9fe41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/9ae44947281eb16213e3db6a280916af9e504f46", - "reference": "9ae44947281eb16213e3db6a280916af9e504f46", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/dfb0acb6803eb714f05d97dd4c5abe6d5fa9fe41", + "reference": "dfb0acb6803eb714f05d97dd4c5abe6d5fa9fe41", "shasum": "" }, "require": { @@ -1164,7 +1149,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/7.1" + "source": "https://github.com/symfony/var-exporter/tree/v7.0.4" }, "funding": [ { @@ -1180,13 +1165,13 @@ "type": "tidelift" } ], - "time": "2024-03-13T10:31:14+00:00" + "time": "2024-02-26T10:35:24+00:00" } ], "packages-dev": [ { "name": "composer/pcre", - "version": "dev-main", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", @@ -1206,7 +1191,6 @@ "phpstan/phpstan-strict-rules": "^1.1", "symfony/phpunit-bridge": "^5" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -1258,16 +1242,16 @@ }, { "name": "composer/semver", - "version": "dev-main", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "1d09200268e7d1052ded8e5da9c73c96a63d18f5" + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/1d09200268e7d1052ded8e5da9c73c96a63d18f5", - "reference": "1d09200268e7d1052ded8e5da9c73c96a63d18f5", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", "shasum": "" }, "require": { @@ -1277,7 +1261,6 @@ "phpstan/phpstan": "^1.4", "symfony/phpunit-bridge": "^4.2 || ^5" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -1320,7 +1303,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/main" + "source": "https://github.com/composer/semver/tree/3.4.0" }, "funding": [ { @@ -1336,7 +1319,7 @@ "type": "tidelift" } ], - "time": "2023-08-31T12:20:31+00:00" + "time": "2023-08-31T09:50:34+00:00" }, { "name": "composer/xdebug-handler", @@ -1499,16 +1482,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.x-dev", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "2f5294676c802a62b0549f6bc8983f14294ce369" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/2f5294676c802a62b0549f6bc8983f14294ce369", - "reference": "2f5294676c802a62b0549f6bc8983f14294ce369", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -1516,15 +1499,13 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3 <3.2.2" + "doctrine/common": "<2.13.3 || >=3,<3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", - "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, - "default-branch": true, "type": "library", "autoload": { "files": [ @@ -1548,7 +1529,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.x" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -1556,20 +1537,20 @@ "type": "tidelift" } ], - "time": "2024-02-10T11:10:03+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "nikic/php-parser", - "version": "dev-master", + "version": "v5.0.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "09691fc86e9e9bf2b936dcc9d1590a3061d223a7" + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/09691fc86e9e9bf2b936dcc9d1590a3061d223a7", - "reference": "09691fc86e9e9bf2b936dcc9d1590a3061d223a7", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", "shasum": "" }, "require": { @@ -1582,7 +1563,6 @@ "ircmaxell/php-yacc": "^0.0.7", "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, - "default-branch": true, "bin": [ "bin/php-parse" ], @@ -1613,13 +1593,13 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/master" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" }, - "time": "2024-03-13T19:24:50+00:00" + "time": "2024-03-05T20:51:40+00:00" }, { "name": "phar-io/manifest", - "version": "dev-master", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", @@ -1639,7 +1619,6 @@ "phar-io/version": "^3.0.1", "php": "^7.2 || ^8.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -1904,16 +1883,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "dev-main", + "version": "11.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "4f6cd757b8fd04721e47980a1a4ddd8734cad024" + "reference": "7e35a2cbcabac0e6865fd373742ea432a3c34f92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f6cd757b8fd04721e47980a1a4ddd8734cad024", - "reference": "4f6cd757b8fd04721e47980a1a4ddd8734cad024", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e35a2cbcabac0e6865fd373742ea432a3c34f92", + "reference": "7e35a2cbcabac0e6865fd373742ea432a3c34f92", "shasum": "" }, "require": { @@ -1938,7 +1917,6 @@ "ext-pcov": "PHP extension that provides line coverage", "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -1971,7 +1949,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/main" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.3" }, "funding": [ { @@ -1979,20 +1957,20 @@ "type": "github" } ], - "time": "2024-03-13T12:30:41+00:00" + "time": "2024-03-12T15:35:40+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "dev-main", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "0550ec5e99071c5c531337a1d7f2a5d520f18cd2" + "reference": "99e95c94ad9500daca992354fa09d7b99abe2210" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/0550ec5e99071c5c531337a1d7f2a5d520f18cd2", - "reference": "0550ec5e99071c5c531337a1d7f2a5d520f18cd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/99e95c94ad9500daca992354fa09d7b99abe2210", + "reference": "99e95c94ad9500daca992354fa09d7b99abe2210", "shasum": "" }, "require": { @@ -2001,7 +1979,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2033,7 +2010,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/main" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.0.0" }, "funding": [ { @@ -2041,20 +2018,20 @@ "type": "github" } ], - "time": "2024-03-11T06:12:34+00:00" + "time": "2024-02-02T06:05:04+00:00" }, { "name": "phpunit/php-invoker", - "version": "dev-main", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "56a1a314aa7dbd2de32d7a14c8cec6eff196d179" + "reference": "5d8d9355a16d8cc5a1305b0a85342cfa420612be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/56a1a314aa7dbd2de32d7a14c8cec6eff196d179", - "reference": "56a1a314aa7dbd2de32d7a14c8cec6eff196d179", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5d8d9355a16d8cc5a1305b0a85342cfa420612be", + "reference": "5d8d9355a16d8cc5a1305b0a85342cfa420612be", "shasum": "" }, "require": { @@ -2067,7 +2044,6 @@ "suggest": { "ext-pcntl": "*" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2098,7 +2074,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/main" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.0" }, "funding": [ { @@ -2106,20 +2082,20 @@ "type": "github" } ], - "time": "2024-03-11T06:13:16+00:00" + "time": "2024-02-02T06:05:50+00:00" }, { "name": "phpunit/php-text-template", - "version": "dev-main", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "0ba24924c8e8afb166e068dfb4447e3915497396" + "reference": "d38f6cbff1cdb6f40b03c9811421561668cc133e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0ba24924c8e8afb166e068dfb4447e3915497396", - "reference": "0ba24924c8e8afb166e068dfb4447e3915497396", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/d38f6cbff1cdb6f40b03c9811421561668cc133e", + "reference": "d38f6cbff1cdb6f40b03c9811421561668cc133e", "shasum": "" }, "require": { @@ -2128,7 +2104,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2159,7 +2134,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/main" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.0" }, "funding": [ { @@ -2167,20 +2142,20 @@ "type": "github" } ], - "time": "2024-03-11T06:13:53+00:00" + "time": "2024-02-02T06:06:56+00:00" }, { "name": "phpunit/php-timer", - "version": "dev-main", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "0c4fd4941bdd8c2346b535f9a41724ece9effe3e" + "reference": "8a59d9e25720482ee7fcdf296595e08795b84dc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/0c4fd4941bdd8c2346b535f9a41724ece9effe3e", - "reference": "0c4fd4941bdd8c2346b535f9a41724ece9effe3e", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8a59d9e25720482ee7fcdf296595e08795b84dc5", + "reference": "8a59d9e25720482ee7fcdf296595e08795b84dc5", "shasum": "" }, "require": { @@ -2189,7 +2164,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2220,7 +2194,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", "security": "https://github.com/sebastianbergmann/php-timer/security/policy", - "source": "https://github.com/sebastianbergmann/php-timer/tree/main" + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.0" }, "funding": [ { @@ -2228,20 +2202,20 @@ "type": "github" } ], - "time": "2024-03-11T06:14:27+00:00" + "time": "2024-02-02T06:08:01+00:00" }, { "name": "phpunit/phpunit", - "version": "dev-main", + "version": "11.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "6185f2add144860ab8d8c3bcbacc57cf77c2a54e" + "reference": "6af32d7938fc366f86e49a5f5ebb314018d1b1fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6185f2add144860ab8d8c3bcbacc57cf77c2a54e", - "reference": "6185f2add144860ab8d8c3bcbacc57cf77c2a54e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6af32d7938fc366f86e49a5f5ebb314018d1b1fb", + "reference": "6af32d7938fc366f86e49a5f5ebb314018d1b1fb", "shasum": "" }, "require": { @@ -2274,14 +2248,13 @@ "suggest": { "ext-soap": "To be able to generate mocks based on WSDL files" }, - "default-branch": true, "bin": [ "phpunit" ], "type": "library", "extra": { "branch-alias": { - "dev-main": "11.1-dev" + "dev-main": "11.0-dev" } }, "autoload": { @@ -2313,7 +2286,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/main" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.0.6" }, "funding": [ { @@ -2329,29 +2302,25 @@ "type": "tidelift" } ], - "time": "2024-03-14T08:05:01+00:00" + "time": "2024-03-12T15:40:01+00:00" }, { "name": "psr/event-dispatcher", - "version": "dev-master", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "977ffcf551e3bfb73d90aac3e8e1583fd8d2f89a" + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/977ffcf551e3bfb73d90aac3e8e1583fd8d2f89a", - "reference": "977ffcf551e3bfb73d90aac3e8e1583fd8d2f89a", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", "shasum": "" }, "require": { "php": ">=7.2.0" }, - "suggest": { - "fig/event-dispatcher-util": "Provides some useful PSR-14 utilities" - }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2370,7 +2339,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "homepage": "http://www.php-fig.org/" } ], "description": "Standard interfaces for event handling.", @@ -2380,22 +2349,23 @@ "psr-14" ], "support": { - "source": "https://github.com/php-fig/event-dispatcher" + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" }, - "time": "2023-09-22T11:10:57+00:00" + "time": "2019-01-08T18:20:26+00:00" }, { "name": "sebastian/cli-parser", - "version": "dev-main", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "deca8c195cf91ed76d7265854851941f21e35fac" + "reference": "00a74d5568694711f0222e54fb281e1d15fdf04a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/deca8c195cf91ed76d7265854851941f21e35fac", - "reference": "deca8c195cf91ed76d7265854851941f21e35fac", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/00a74d5568694711f0222e54fb281e1d15fdf04a", + "reference": "00a74d5568694711f0222e54fb281e1d15fdf04a", "shasum": "" }, "require": { @@ -2404,7 +2374,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2432,7 +2401,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/main" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.1" }, "funding": [ { @@ -2440,20 +2409,20 @@ "type": "github" } ], - "time": "2024-03-11T06:01:13+00:00" + "time": "2024-03-02T07:26:58+00:00" }, { "name": "sebastian/code-unit", - "version": "dev-main", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "a4c11e6e8b3e615a1d44adea5b98f4e5495810f4" + "reference": "6634549cb8d702282a04a774e36a7477d2bd9015" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a4c11e6e8b3e615a1d44adea5b98f4e5495810f4", - "reference": "a4c11e6e8b3e615a1d44adea5b98f4e5495810f4", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/6634549cb8d702282a04a774e36a7477d2bd9015", + "reference": "6634549cb8d702282a04a774e36a7477d2bd9015", "shasum": "" }, "require": { @@ -2462,7 +2431,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2490,7 +2458,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/main" + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.0" }, "funding": [ { @@ -2498,20 +2466,20 @@ "type": "github" } ], - "time": "2024-03-11T06:02:24+00:00" + "time": "2024-02-02T05:50:41+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "dev-main", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "e2f3e47034bde0b34d95dea2ae03066f88ad3ce9" + "reference": "df80c875d3e459b45c6039e4d9b71d4fbccae25d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/e2f3e47034bde0b34d95dea2ae03066f88ad3ce9", - "reference": "e2f3e47034bde0b34d95dea2ae03066f88ad3ce9", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/df80c875d3e459b45c6039e4d9b71d4fbccae25d", + "reference": "df80c875d3e459b45c6039e4d9b71d4fbccae25d", "shasum": "" }, "require": { @@ -2520,7 +2488,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2547,7 +2514,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/main" + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.0" }, "funding": [ { @@ -2555,20 +2522,20 @@ "type": "github" } ], - "time": "2024-03-11T06:03:40+00:00" + "time": "2024-02-02T05:52:17+00:00" }, { "name": "sebastian/comparator", - "version": "dev-main", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "37cefdb60897d96dd22619a5f382489b11b1230e" + "reference": "bd0f2fa5b9257c69903537b266ccb80fcf940db8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/37cefdb60897d96dd22619a5f382489b11b1230e", - "reference": "37cefdb60897d96dd22619a5f382489b11b1230e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/bd0f2fa5b9257c69903537b266ccb80fcf940db8", + "reference": "bd0f2fa5b9257c69903537b266ccb80fcf940db8", "shasum": "" }, "require": { @@ -2581,7 +2548,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2625,7 +2591,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/main" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.0.0" }, "funding": [ { @@ -2633,20 +2599,20 @@ "type": "github" } ], - "time": "2024-03-11T06:04:32+00:00" + "time": "2024-02-02T05:53:45+00:00" }, { "name": "sebastian/complexity", - "version": "dev-main", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "0a32e85aacbf48529d054510dfd009d730e778d4" + "reference": "88a434ad86150e11a606ac4866b09130712671f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/0a32e85aacbf48529d054510dfd009d730e778d4", - "reference": "0a32e85aacbf48529d054510dfd009d730e778d4", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/88a434ad86150e11a606ac4866b09130712671f0", + "reference": "88a434ad86150e11a606ac4866b09130712671f0", "shasum": "" }, "require": { @@ -2656,7 +2622,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2684,7 +2649,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/main" + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.0" }, "funding": [ { @@ -2692,20 +2657,20 @@ "type": "github" } ], - "time": "2024-03-11T06:05:20+00:00" + "time": "2024-02-02T05:55:19+00:00" }, { "name": "sebastian/diff", - "version": "dev-main", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "2c0b41c47928dbe8916685299f43bb55df701863" + "reference": "ab83243ecc233de5655b76f577711de9f842e712" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/2c0b41c47928dbe8916685299f43bb55df701863", - "reference": "2c0b41c47928dbe8916685299f43bb55df701863", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ab83243ecc233de5655b76f577711de9f842e712", + "reference": "ab83243ecc233de5655b76f577711de9f842e712", "shasum": "" }, "require": { @@ -2715,7 +2680,6 @@ "phpunit/phpunit": "^11.0", "symfony/process": "^4.2 || ^5" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2752,7 +2716,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/main" + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.1" }, "funding": [ { @@ -2760,20 +2724,20 @@ "type": "github" } ], - "time": "2024-03-11T06:06:37+00:00" + "time": "2024-03-02T07:30:33+00:00" }, { "name": "sebastian/environment", - "version": "dev-main", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "b7d0046d9ebbf28467ad38e2b544d824e2dd216e" + "reference": "100d8b855d7180f79f9a9a5c483f2d960581c3ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b7d0046d9ebbf28467ad38e2b544d824e2dd216e", - "reference": "b7d0046d9ebbf28467ad38e2b544d824e2dd216e", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/100d8b855d7180f79f9a9a5c483f2d960581c3ea", + "reference": "100d8b855d7180f79f9a9a5c483f2d960581c3ea", "shasum": "" }, "require": { @@ -2785,7 +2749,6 @@ "suggest": { "ext-posix": "*" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2817,7 +2780,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/main" + "source": "https://github.com/sebastianbergmann/environment/tree/7.0.0" }, "funding": [ { @@ -2825,20 +2788,20 @@ "type": "github" } ], - "time": "2024-03-11T06:07:19+00:00" + "time": "2024-02-02T05:57:54+00:00" }, { "name": "sebastian/exporter", - "version": "dev-main", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "2861697fb36cd78362a0f0ccc145e124b6641b91" + "reference": "f291e5a317c321c0381fa9ecc796fa2d21b186da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/2861697fb36cd78362a0f0ccc145e124b6641b91", - "reference": "2861697fb36cd78362a0f0ccc145e124b6641b91", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/f291e5a317c321c0381fa9ecc796fa2d21b186da", + "reference": "f291e5a317c321c0381fa9ecc796fa2d21b186da", "shasum": "" }, "require": { @@ -2849,7 +2812,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2896,7 +2858,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/main" + "source": "https://github.com/sebastianbergmann/exporter/tree/6.0.1" }, "funding": [ { @@ -2904,20 +2866,20 @@ "type": "github" } ], - "time": "2024-03-11T06:08:01+00:00" + "time": "2024-03-02T07:28:20+00:00" }, { "name": "sebastian/global-state", - "version": "dev-main", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "556973360ef0b1318be361298b7d25bde5b5cc61" + "reference": "c3a307e832f2e69c7ef869e31fc644fde0e7cb3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/556973360ef0b1318be361298b7d25bde5b5cc61", - "reference": "556973360ef0b1318be361298b7d25bde5b5cc61", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/c3a307e832f2e69c7ef869e31fc644fde0e7cb3e", + "reference": "c3a307e832f2e69c7ef869e31fc644fde0e7cb3e", "shasum": "" }, "require": { @@ -2929,7 +2891,6 @@ "ext-dom": "*", "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -2959,7 +2920,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/main" + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.1" }, "funding": [ { @@ -2967,20 +2928,20 @@ "type": "github" } ], - "time": "2024-03-11T06:08:33+00:00" + "time": "2024-03-02T07:32:10+00:00" }, { "name": "sebastian/lines-of-code", - "version": "dev-main", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "09754e884d6278965cfd89f7daf557b1b0f53e26" + "reference": "376c5b3f6b43c78fdc049740bca76a7c846706c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/09754e884d6278965cfd89f7daf557b1b0f53e26", - "reference": "09754e884d6278965cfd89f7daf557b1b0f53e26", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/376c5b3f6b43c78fdc049740bca76a7c846706c0", + "reference": "376c5b3f6b43c78fdc049740bca76a7c846706c0", "shasum": "" }, "require": { @@ -2990,7 +2951,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -3018,7 +2978,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/main" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.0" }, "funding": [ { @@ -3026,20 +2986,20 @@ "type": "github" } ], - "time": "2024-03-11T06:09:07+00:00" + "time": "2024-02-02T06:00:36+00:00" }, { "name": "sebastian/object-enumerator", - "version": "dev-main", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "e3c83cdbfb07ca6eba997e33964aa48b917b62cc" + "reference": "f75f6c460da0bbd9668f43a3dde0ec0ba7faa678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e3c83cdbfb07ca6eba997e33964aa48b917b62cc", - "reference": "e3c83cdbfb07ca6eba997e33964aa48b917b62cc", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f75f6c460da0bbd9668f43a3dde0ec0ba7faa678", + "reference": "f75f6c460da0bbd9668f43a3dde0ec0ba7faa678", "shasum": "" }, "require": { @@ -3050,7 +3010,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -3077,7 +3036,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/main" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.0" }, "funding": [ { @@ -3085,20 +3044,20 @@ "type": "github" } ], - "time": "2024-03-11T06:09:46+00:00" + "time": "2024-02-02T06:01:29+00:00" }, { "name": "sebastian/object-reflector", - "version": "dev-main", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "af5905e3e9f15435976cc632f8ba481d72859db6" + "reference": "bb2a6255d30853425fd38f032eb64ced9f7f132d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/af5905e3e9f15435976cc632f8ba481d72859db6", - "reference": "af5905e3e9f15435976cc632f8ba481d72859db6", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/bb2a6255d30853425fd38f032eb64ced9f7f132d", + "reference": "bb2a6255d30853425fd38f032eb64ced9f7f132d", "shasum": "" }, "require": { @@ -3107,7 +3066,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -3134,7 +3092,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/main" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.0" }, "funding": [ { @@ -3142,20 +3100,20 @@ "type": "github" } ], - "time": "2024-03-11T06:11:41+00:00" + "time": "2024-02-02T06:02:18+00:00" }, { "name": "sebastian/recursion-context", - "version": "dev-main", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "80bc89ce7dc13751e6de4d372babb5af00d0b7e7" + "reference": "b75224967b5a466925c6d54e68edd0edf8dd4ed4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/80bc89ce7dc13751e6de4d372babb5af00d0b7e7", - "reference": "80bc89ce7dc13751e6de4d372babb5af00d0b7e7", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b75224967b5a466925c6d54e68edd0edf8dd4ed4", + "reference": "b75224967b5a466925c6d54e68edd0edf8dd4ed4", "shasum": "" }, "require": { @@ -3164,7 +3122,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -3199,7 +3156,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/main" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.0" }, "funding": [ { @@ -3207,20 +3164,20 @@ "type": "github" } ], - "time": "2024-03-11T06:15:06+00:00" + "time": "2024-02-02T06:08:48+00:00" }, { "name": "sebastian/type", - "version": "dev-main", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "99ef27ad359e0adec102cd00203969a4c491dc91" + "reference": "b8502785eb3523ca0dd4afe9ca62235590020f3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/99ef27ad359e0adec102cd00203969a4c491dc91", - "reference": "99ef27ad359e0adec102cd00203969a4c491dc91", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8502785eb3523ca0dd4afe9ca62235590020f3f", + "reference": "b8502785eb3523ca0dd4afe9ca62235590020f3f", "shasum": "" }, "require": { @@ -3229,7 +3186,6 @@ "require-dev": { "phpunit/phpunit": "^11.0" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -3257,7 +3213,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/main" + "source": "https://github.com/sebastianbergmann/type/tree/5.0.0" }, "funding": [ { @@ -3265,26 +3221,25 @@ "type": "github" } ], - "time": "2024-03-11T06:15:36+00:00" + "time": "2024-02-02T06:09:34+00:00" }, { "name": "sebastian/version", - "version": "dev-main", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "e83d0c904cb3bf6232a7fb7777b124541540f075" + "reference": "13999475d2cb1ab33cb73403ba356a814fdbb001" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/e83d0c904cb3bf6232a7fb7777b124541540f075", - "reference": "e83d0c904cb3bf6232a7fb7777b124541540f075", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/13999475d2cb1ab33cb73403ba356a814fdbb001", + "reference": "13999475d2cb1ab33cb73403ba356a814fdbb001", "shasum": "" }, "require": { "php": ">=8.2" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -3312,7 +3267,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/version/issues", "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/main" + "source": "https://github.com/sebastianbergmann/version/tree/5.0.0" }, "funding": [ { @@ -3320,20 +3275,20 @@ "type": "github" } ], - "time": "2024-03-11T06:16:29+00:00" + "time": "2024-02-02T06:10:47+00:00" }, { "name": "symfony/console", - "version": "7.1.x-dev", + "version": "v7.0.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "31c74a59d8bffdcec73adb43b398f6942f7dc26b" + "reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/31c74a59d8bffdcec73adb43b398f6942f7dc26b", - "reference": "31c74a59d8bffdcec73adb43b398f6942f7dc26b", + "url": "https://api.github.com/repos/symfony/console/zipball/6b099f3306f7c9c2d2786ed736d0026b2903205f", + "reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f", "shasum": "" }, "require": { @@ -3397,7 +3352,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/7.1" + "source": "https://github.com/symfony/console/tree/v7.0.4" }, "funding": [ { @@ -3413,20 +3368,20 @@ "type": "tidelift" } ], - "time": "2024-03-04T12:49:13+00:00" + "time": "2024-02-22T20:27:20+00:00" }, { "name": "symfony/event-dispatcher", - "version": "7.1.x-dev", + "version": "v7.0.3", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "5bb99ba359e39909230a22e343271e9385bbee08" + "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/5bb99ba359e39909230a22e343271e9385bbee08", - "reference": "5bb99ba359e39909230a22e343271e9385bbee08", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/834c28d533dd0636f910909d01b9ff45cc094b5e", + "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e", "shasum": "" }, "require": { @@ -3477,7 +3432,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/7.1" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.3" }, "funding": [ { @@ -3493,31 +3448,30 @@ "type": "tidelift" } ], - "time": "2024-01-23T15:06:13+00:00" + "time": "2024-01-23T15:02:46+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "dev-main", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "4d4ea14a8d31bc995e29bdbd566ac07c9fd004f5" + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/4d4ea14a8d31bc995e29bdbd566ac07c9fd004f5", - "reference": "4d4ea14a8d31bc995e29bdbd566ac07c9fd004f5", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", "shasum": "" }, "require": { "php": ">=8.1", "psr/event-dispatcher": "^1" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -3554,7 +3508,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/main" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.0" }, "funding": [ { @@ -3570,20 +3524,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T15:06:13+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/filesystem", - "version": "7.1.x-dev", + "version": "v7.0.3", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "60d42aa14bdecc7f488988430a144fed084c81e2" + "reference": "2890e3a825bc0c0558526c04499c13f83e1b6b12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/60d42aa14bdecc7f488988430a144fed084c81e2", - "reference": "60d42aa14bdecc7f488988430a144fed084c81e2", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/2890e3a825bc0c0558526c04499c13f83e1b6b12", + "reference": "2890e3a825bc0c0558526c04499c13f83e1b6b12", "shasum": "" }, "require": { @@ -3617,7 +3571,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/7.1" + "source": "https://github.com/symfony/filesystem/tree/v7.0.3" }, "funding": [ { @@ -3633,20 +3587,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T15:06:13+00:00" + "time": "2024-01-23T15:02:46+00:00" }, { "name": "symfony/finder", - "version": "7.1.x-dev", + "version": "v7.0.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "beeac2ba50a5dd729d8a23507ad09cdd0f82110c" + "reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/beeac2ba50a5dd729d8a23507ad09cdd0f82110c", - "reference": "beeac2ba50a5dd729d8a23507ad09cdd0f82110c", + "url": "https://api.github.com/repos/symfony/finder/zipball/6e5688d69f7cfc4ed4a511e96007e06c2d34ce56", + "reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56", "shasum": "" }, "require": { @@ -3681,7 +3635,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/7.1" + "source": "https://github.com/symfony/finder/tree/v7.0.0" }, "funding": [ { @@ -3697,20 +3651,20 @@ "type": "tidelift" } ], - "time": "2024-01-16T21:21:05+00:00" + "time": "2023-10-31T17:59:56+00:00" }, { "name": "symfony/options-resolver", - "version": "7.1.x-dev", + "version": "v7.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "fac02432c8616006aa9f1d6274440a58a0e90f4c" + "reference": "700ff4096e346f54cb628ea650767c8130f1001f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/fac02432c8616006aa9f1d6274440a58a0e90f4c", - "reference": "fac02432c8616006aa9f1d6274440a58a0e90f4c", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/700ff4096e346f54cb628ea650767c8130f1001f", + "reference": "700ff4096e346f54cb628ea650767c8130f1001f", "shasum": "" }, "require": { @@ -3748,7 +3702,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/7.1" + "source": "https://github.com/symfony/options-resolver/tree/v7.0.0" }, "funding": [ { @@ -3764,11 +3718,11 @@ "type": "tidelift" } ], - "time": "2024-02-21T09:01:35+00:00" + "time": "2023-08-08T10:20:21+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "1.x-dev", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -3789,7 +3743,6 @@ "suggest": { "ext-ctype": "For best performance" }, - "default-branch": true, "type": "library", "extra": { "thanks": { @@ -3848,7 +3801,7 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "1.x-dev", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -3866,7 +3819,6 @@ "suggest": { "ext-intl": "For best performance" }, - "default-branch": true, "type": "library", "extra": { "thanks": { @@ -3927,7 +3879,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "1.x-dev", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -3945,7 +3897,6 @@ "suggest": { "ext-intl": "For best performance" }, - "default-branch": true, "type": "library", "extra": { "thanks": { @@ -4009,7 +3960,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "1.x-dev", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -4030,7 +3981,6 @@ "suggest": { "ext-mbstring": "For best performance" }, - "default-branch": true, "type": "library", "extra": { "thanks": { @@ -4090,7 +4040,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "1.x-dev", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -4105,7 +4055,6 @@ "require": { "php": ">=7.1" }, - "default-branch": true, "type": "library", "extra": { "thanks": { @@ -4171,7 +4120,7 @@ }, { "name": "symfony/polyfill-php81", - "version": "1.x-dev", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -4186,7 +4135,6 @@ "require": { "php": ">=7.1" }, - "default-branch": true, "type": "library", "extra": { "thanks": { @@ -4248,16 +4196,16 @@ }, { "name": "symfony/process", - "version": "7.1.x-dev", + "version": "v7.0.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "ef909caff8547b26f72dff049ea24f9b07a3701a" + "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/ef909caff8547b26f72dff049ea24f9b07a3701a", - "reference": "ef909caff8547b26f72dff049ea24f9b07a3701a", + "url": "https://api.github.com/repos/symfony/process/zipball/0e7727191c3b71ebec6d529fa0e50a01ca5679e9", + "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9", "shasum": "" }, "require": { @@ -4289,7 +4237,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/7.1" + "source": "https://github.com/symfony/process/tree/v7.0.4" }, "funding": [ { @@ -4305,20 +4253,20 @@ "type": "tidelift" } ], - "time": "2024-02-22T20:28:06+00:00" + "time": "2024-02-22T20:27:20+00:00" }, { "name": "symfony/stopwatch", - "version": "7.1.x-dev", + "version": "v7.0.3", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "b7fa24bb627fcb7e104669bca6861f18e8814a9b" + "reference": "983900d6fddf2b0cbaacacbbad07610854bd8112" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b7fa24bb627fcb7e104669bca6861f18e8814a9b", - "reference": "b7fa24bb627fcb7e104669bca6861f18e8814a9b", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/983900d6fddf2b0cbaacacbbad07610854bd8112", + "reference": "983900d6fddf2b0cbaacacbbad07610854bd8112", "shasum": "" }, "require": { @@ -4351,7 +4299,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/7.1" + "source": "https://github.com/symfony/stopwatch/tree/v7.0.3" }, "funding": [ { @@ -4367,20 +4315,20 @@ "type": "tidelift" } ], - "time": "2024-02-21T09:01:35+00:00" + "time": "2024-01-23T15:02:46+00:00" }, { "name": "symfony/string", - "version": "7.1.x-dev", + "version": "v7.0.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ef964850371aca5def9fc8a3059d9b1690bb3b6c" + "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ef964850371aca5def9fc8a3059d9b1690bb3b6c", - "reference": "ef964850371aca5def9fc8a3059d9b1690bb3b6c", + "url": "https://api.github.com/repos/symfony/string/zipball/f5832521b998b0bec40bee688ad5de98d4cf111b", + "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b", "shasum": "" }, "require": { @@ -4394,7 +4342,6 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", @@ -4438,7 +4385,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/7.1" + "source": "https://github.com/symfony/string/tree/v7.0.4" }, "funding": [ { @@ -4454,7 +4401,7 @@ "type": "tidelift" } ], - "time": "2024-03-08T12:55:53+00:00" + "time": "2024-02-01T13:17:36+00:00" }, { "name": "theseer/tokenizer", @@ -4512,7 +4459,7 @@ "stability-flags": { "friendsofphp/php-cs-fixer": 20 }, - "prefer-stable": false, + "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.2 <8.5" diff --git a/docs/basic-usage.md b/docs/basic-usage.md index 94367bd..ff0bcd0 100644 --- a/docs/basic-usage.md +++ b/docs/basic-usage.md @@ -2,14 +2,19 @@ LibrariesIO is a simple API wrapper/client for the Libraries.io API. Since this class solely relies on the libraries.io API, you will need an api key from the libraries.io API service. Signing up for an account is free. You can find your api key in your [account](https://libraries.io/account). +## Import the library + ```php -// All at once use Esi\LibrariesIO\LibrariesIO; ``` -Then just use whichever endpoint you need. +A `Utils` class is also available which can take the API response and covert it to an array, object, or give you the raw json data. All methods of `Utils` are static. To use `Utils`, import it using: + +```php +use Esi\LibrariesIO\Utils; +``` -Endpoints: +## Available Endpoints * [Platform](platform.md) * [Project](project.md) @@ -24,11 +29,12 @@ See more: [Platform](platform.md) ```php use Esi\LibrariesIO\LibrariesIO; +use Esi\LibrariesIO\Utils; $api = new LibrariesIO('..yourapikey..', \sys_get_temp_dir()); $response = $api->platform(); -print_r($api->raw($response)); +print_r(Utils::raw($response)); ``` Would return something like: diff --git a/docs/changelog.md b/docs/changelog.md index 007b925..4a012ec 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,14 +1,31 @@ -## CHANGELOG +# CHANGELOG A not so exhaustive list of changes for each release. For a more detailed listing of changes between each version, -you can use the following url: https://github.com/ericsizemore/librariesio/compare/v1.0.0...v1.1.0. +you can use the following url: https://github.com/ericsizemore/librariesio/compare/v1.0.0...v2.0.0. Simply replace the version numbers depending on which set of changes you wish to see. -### 1.1.0 (2023-12-29) +## 1.1.1 (2024-03-14) + +### Changed + + * Updated/refactored some code to reduce duplicate checks/etc. throughout. + * CS improvements/fixes. + +### Added + + * Added PHP-CS-Fixer to dev dependencies. + +### Removed + + * Cleaned up some doc blocks and removed some unnecessary comments. + + +## 1.1.0 (2023-12-29) + +### Changed - * Added `subscription()` to handle adding, updating, checking and removing a subscription to a project. * Updated `makeRequest()` with a `$method` parameter to handle post, put, and delete requests in addition to get. * Visibility changed to `protected` for: * endpointParameters() @@ -17,6 +34,15 @@ Simply replace the version numbers depending on which set of changes you wish to * Converted line endings to linux, some files snuck through with Windows line endings * Documentation updated -### 1.0.0 (2023-12-25) +### Added + + * Added `subscription()` to handle adding, updating, checking and removing a subscription to a project. + +### Removed + + * None + + +## 1.0.0 (2023-12-25) * Initial release diff --git a/docs/index.md b/docs/index.md index 339c961..a03540c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -37,7 +37,7 @@ require 'vendor/autoload.php'; ?> ``` -For more information see the [installation](installation.md) docs. +For more information see the [installation](docs/installation.md) docs. ## Basic Usage @@ -87,11 +87,12 @@ As an example, let's say you want to get a list of the available platforms. To d platform(); -print_r($api->toArray($response)); +print_r(Utils::toArray($response)); /* Array @@ -119,7 +120,7 @@ Array ?> ``` -For more information see the [basic usage](basic-usage.md) docs. +For more information see the [basic usage](docs/basic-usage.md) docs. ## Testing diff --git a/docs/installation.md b/docs/installation.md index b13e683..3c2976a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -14,6 +14,7 @@ Make sure you include `vendor/autoload.php` in your application. To make all of ```php use Esi\LibrariesIO\LibrariesIO; +use Esi\LibrariesIO\Utils; ``` For more information check out the [basic-usage](basic-usage.md) documenation. Further reading: @@ -22,4 +23,7 @@ For more information check out the [basic-usage](basic-usage.md) documenation. F * [Platform](platform.md) * [Project](project.md) * [Repository](repository.md) - * [User](user.md) \ No newline at end of file + * [User](user.md) + +* Extras: + * [Utils](utils.md) \ No newline at end of file diff --git a/docs/license.md b/docs/license.md index 81bc9fc..f9d6301 100644 --- a/docs/license.md +++ b/docs/license.md @@ -3,7 +3,7 @@ The MIT License (MIT) The MIT License (MIT) -Copyright (c) 2023 Eric Sizemore +Copyright (c) 2023-2024 Eric Sizemore Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/platform.md b/docs/platform.md index 7929809..42c92dc 100644 --- a/docs/platform.md +++ b/docs/platform.md @@ -6,11 +6,11 @@ /** * Performs a request to the 'platforms' endpoint. * - * @param string $endpoint - * @return ResponseInterface - * @throws InvalidArgumentException|ClientException|GuzzleException + * @throws ClientException + * @throws GuzzleException + * @throws RateLimitExceededException */ -public function platform(string $endpoint = 'platforms'): ResponseInterface; +public function platform(): ResponseInterface; ``` Get a list of supported package managers. @@ -23,6 +23,7 @@ More information [here](https://libraries.io/api#platforms) ```php use Esi\LibrariesIO\LibrariesIO; +use Esi\LibrariesIO\Utils; // Obviously you would want to pass your API key to the constructor, along with // a folder/path to be used for caching requests if desired. @@ -33,14 +34,14 @@ $response = $api->platform(); // From here you have a few options depending on how you need or want the data. -// For just the raw json date, we can use raw() -$json = $api->raw($response); +// For just the raw json date, we can use Utils::raw() +$json = Utils::raw($response); -// To have the json decoded and handed back to you as an array, use toArray() -$json = $api->toArray($response); +// To have the json decoded and handed back to you as an array, use Utils::toArray() +$json = Utils::toArray($response); -// Or, to have it returned to you as an object (an \stdClass object), use toObject() -$json = $api->toObject($response); +// Or, to have it returned to you as an object (an \stdClass object), use Utils::toObject() +$json = Utils::toObject($response); // It is important to note that raw(), toArray(), and toObject() must have the $response as an argument. // $response will be an instance of '\Psr\Http\Message\ResponseInterface' @@ -48,7 +49,7 @@ $json = $api->toObject($response); // It is not recommended to attempt calling either of the to* functions back to back ``` -The call to `platform()` and then using `raw()` will return something like: +The call to `platform()` and then using `Utils::raw()` will return something like: ```json [ diff --git a/docs/project.md b/docs/project.md index ca9890b..497fad4 100644 --- a/docs/project.md +++ b/docs/project.md @@ -7,10 +7,12 @@ * Performs a request to the 'project' endpoint and a subset endpoint, which can be: * contributors, dependencies, dependent_repositories, dependents, search, sourcerank, or project * - * @param string $endpoint - * @param array $options - * @return ResponseInterface - * @throws InvalidArgumentException|ClientException|GuzzleException + * @param array|array $options + * + * @throws InvalidEndpointException + * @throws ClientException + * @throws GuzzleException + * @throws RateLimitExceededException */ public function project(string $endpoint, array $options): ResponseInterface; ``` @@ -105,6 +107,7 @@ An example using the `project()` method with the 'project' `$endpoint` parameter ```php use Esi\LibrariesIO\LibrariesIO; +use Esi\LibrariesIO\Utils; // Obviously you would want to pass your API key to the constructor, along with // a folder/path to be used for caching requests if desired. @@ -116,14 +119,14 @@ $response = $api->project('project', ['platform' => 'npm', 'name' => 'base62']); // From here you have a few options depending on how you need or want the data. -// For just the raw json date, we can use raw() -$json = $api->raw($response); +// For just the raw json date, we can use Utils::raw() +$json = Utils::raw($response); -// To have the json decoded and handed back to you as an array, use toArray() -$json = $api->toArray($response); +// To have the json decoded and handed back to you as an array, use Utils::toArray() +$json = Utils::toArray($response); -// Or, to have it returned to you as an object (an \stdClass object), use toObject() -$json = $api->toObject($response); +// Or, to have it returned to you as an object (an \stdClass object), use Utils::toObject() +$json = Utils::toObject($response); // It is important to note that raw(), toArray(), and toObject() must have the $response as an argument. // $response will be an instance of '\Psr\Http\Message\ResponseInterface' @@ -131,7 +134,7 @@ $json = $api->toObject($response); // It is not recommended to attempt calling either of the to* functions back to back ``` -The call to `project()` using the 'project' endpoint and then using `raw()` will return something like: +The call to `project()` using the 'project' endpoint and then using `Utils::raw()` will return something like: ```json { diff --git a/docs/repository.md b/docs/repository.md index 74ec113..901fcab 100644 --- a/docs/repository.md +++ b/docs/repository.md @@ -7,10 +7,9 @@ * Performs a request to the 'repository' endpoint and a subset endpoint, which can be: * dependencies, projects, or repository * - * @param string $endpoint - * @param array $options - * @return ResponseInterface - * @throws InvalidArgumentException|ClientException|GuzzleException + * @param array|array $options + * + * @throws InvalidArgumentException|ClientException|GuzzleException|RateLimitExceededException */ public function repository(string $endpoint, array $options): ResponseInterface; ``` @@ -60,6 +59,7 @@ An example using the `repository()` method with the 'repository' `$endpoint` par ```php use Esi\LibrariesIO\LibrariesIO; +use Esi\LibrariesIO\Utils; // Obviously you would want to pass your API key to the constructor, along with // a folder/path to be used for caching requests if desired. @@ -71,14 +71,14 @@ $response = $api->repository('project', ['owner' => 'gruntjs', 'name' => 'grunt' // From here you have a few options depending on how you need or want the data. -// For just the raw json date, we can use raw() -$json = $api->raw($response); +// For just the raw json date, we can use Utils::raw() +$json = Utils::raw($response); -// To have the json decoded and handed back to you as an array, use toArray() -$json = $api->toArray($response); +// To have the json decoded and handed back to you as an array, use Utils::toArray() +$json = Utils::toArray($response); -// Or, to have it returned to you as an object (an \stdClass object), use toObject() -$json = $api->toObject($response); +// Or, to have it returned to you as an object (an \stdClass object), use Utils::toObject() +$json = Utils::toObject($response); // It is important to note that raw(), toArray(), and toObject() must have the $response as an argument. // $response will be an instance of '\Psr\Http\Message\ResponseInterface' @@ -86,7 +86,7 @@ $json = $api->toObject($response); // It is not recommended to attempt calling either of the to* functions back to back ``` -The call to `repository()` using the 'repository' endpoint and then using `raw()` will return something like: +The call to `repository()` using the 'repository' endpoint and then using `Utils::raw()` will return something like: ```json { diff --git a/docs/security.md b/docs/security.md index 7848d03..3395b7e 100644 --- a/docs/security.md +++ b/docs/security.md @@ -4,6 +4,7 @@ | Version | Supported | |---------|--------------------| +| 2.0.x | :white_check_mark: | | 1.1.x | :white_check_mark: | | 1.0.x | :white_check_mark: | diff --git a/docs/subscription.md b/docs/subscription.md index 6b37ed4..acd6a51 100644 --- a/docs/subscription.md +++ b/docs/subscription.md @@ -7,10 +7,9 @@ * Performs a request to the 'subscription' endpoint and a subset endpoint, which can be: * subscribe, check, update, unsubscribe * - * @param string $endpoint - * @param array $options - * @return ResponseInterface - * @throws InvalidArgumentException|ClientException|GuzzleException + * @param array $options + * + * @throws InvalidArgumentException|ClientException|GuzzleException|RateLimitExceededException */ public function subscription(string $endpoint, array $options): ResponseInterface; ``` @@ -73,6 +72,7 @@ An example using the `subscription()` method with the 'subscribe' `$endpoint` pa ```php use Esi\LibrariesIO\LibrariesIO; +use Esi\LibrariesIO\Utils; // Obviously you would want to pass your API key to the constructor, along with // a folder/path to be used for caching requests if desired. @@ -84,14 +84,14 @@ $response = $api->subscription('subscribe', ['platform' => 'npm', 'name' => 'uti // From here you have a few options depending on how you need or want the data. -// For just the raw json date, we can use raw() -$json = $api->raw($response); +// For just the raw json date, we can use Utils::raw() +$json = Utils::raw($response); -// To have the json decoded and handed back to you as an array, use toArray() -$json = $api->toArray($response); +// To have the json decoded and handed back to you as an array, use Utils::toArray() +$json = Utils::toArray($response); -// Or, to have it returned to you as an object (an \stdClass object), use toObject() -$json = $api->toObject($response); +// Or, to have it returned to you as an object (an \stdClass object), use Utils::toObject() +$json = Utils::toObject($response); // It is important to note that raw(), toArray(), and toObject() must have the $response as an argument. // $response will be an instance of '\Psr\Http\Message\ResponseInterface' @@ -99,7 +99,7 @@ $json = $api->toObject($response); // It is not recommended to attempt calling either of the to* functions back to back ``` -The call to `subscription()` using the 'subscribe' endpoint and then using `raw()` will return something like: +The call to `subscription()` using the 'subscribe' endpoint and then using `Utils::raw()` will return something like: ```json { diff --git a/docs/user.md b/docs/user.md index e7509cf..4c14ae8 100644 --- a/docs/user.md +++ b/docs/user.md @@ -7,10 +7,9 @@ * Performs a request to the 'user' endpoint and a subset endpoint, which can be: * dependencies, package_contributions, packages, repositories, repository_contributions, or subscriptions * - * @param string $endpoint - * @param array $options - * @return ResponseInterface - * @throws InvalidArgumentException|ClientException|GuzzleException + * @param array|array $options + * + * @throws InvalidArgumentException|ClientException|GuzzleException|RateLimitExceededException */ public function user(string $endpoint, array $options): ResponseInterface; ``` @@ -96,6 +95,7 @@ An example using the `user()` method with the 'user' `$endpoint` parameter. ```php use Esi\LibrariesIO\LibrariesIO; +use Esi\LibrariesIO\Utils; // Obviously you would want to pass your API key to the constructor, along with // a folder/path to be used for caching requests if desired. @@ -108,14 +108,14 @@ $response = $api->user('user', ['login' => 'andrew']); // From here you have a few options depending on how you need or want the data. -// For just the raw json date, we can use raw() -$json = $api->raw($response); +// For just the raw json date, we can use Utils::raw() +$json = Utils::raw($response); -// To have the json decoded and handed back to you as an array, use toArray() -$json = $api->toArray($response); +// To have the json decoded and handed back to you as an array, use Utils::toArray() +$json = Utils::toArray($response); -// Or, to have it returned to you as an object (an \stdClass object), use toObject() -$json = $api->toObject($response); +// Or, to have it returned to you as an object (an \stdClass object), use Utils::toObject() +$json = Utils::toObject($response); // It is important to note that raw(), toArray(), and toObject() must have the $response as an argument. // $response will be an instance of '\Psr\Http\Message\ResponseInterface' @@ -123,7 +123,7 @@ $json = $api->toObject($response); // It is not recommended to attempt calling either of the to* functions back to back ``` -The call to `user()` using the 'user' endpoint and then using `raw()` will return something like: +The call to `user()` using the 'user' endpoint and then using `Utils::raw()` will return something like: ```json { diff --git a/mkdocs.yml b/mkdocs.yml index cabc1b9..33a117d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,6 +9,11 @@ repo_url: 'https://github.com/ericsizemore/librariesio' theme: readthedocs nav: - Home: 'index.md' + - About: + - 'License': 'license.md' + - 'Changelog': 'changelog.md' + - 'Security': 'security.md' + - 'Code of Conduct': 'code-of-conduct.md' - Installing: 'installation.md' - Basic Usage: 'basic-usage.md' - Endpoints: @@ -17,8 +22,5 @@ nav: - 'Repository': 'repository.md' - 'Subscription': 'subscription.md' - 'User': 'user.md' - - About: - - 'License': 'license.md' - - 'Changelog': 'changelog.md' - - 'Security': 'security.md' - - 'Code of Conduct': 'code-of-conduct.md' + - Extras + - 'Utils': 'utils.md' \ No newline at end of file diff --git a/src/AbstractClient.php b/src/AbstractClient.php new file mode 100644 index 0000000..9c9d597 --- /dev/null +++ b/src/AbstractClient.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Esi\LibrariesIO; + +use Esi\LibrariesIO\Exception\InvalidApiKeyException; +use Esi\LibrariesIO\Exception\RateLimitExceededException; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Exception\InvalidArgumentException; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use Kevinrob\GuzzleCache\{ + CacheMiddleware, + Storage\Psr6CacheStorage, + Strategy\PrivateCacheStrategy +}; +use Psr\Http\Message\ResponseInterface; +use RuntimeException; +use SensitiveParameter; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; + +use function array_filter; +use function array_merge; +use function assert; +use function is_array; +use function is_dir; +use function is_writable; +use function preg_match; +use function sprintf; + +use const ARRAY_FILTER_USE_BOTH; + +abstract class AbstractClient +{ + public const LIB_VERSION = '2.0.0'; + + private const API_URL = 'https://libraries.io/api/'; + + /** + * @see https://libraries.io/account + */ + private readonly string $apiKey; + + private readonly Client $client; + + /** + * @param string $apiKey Your Libraries.io API Key + * @param ?string $cachePath The path to your cache on the filesystem + * @param array $clientOptions An associative array with options to set in the initial config of the Guzzle + * client. + * + * @see https://docs.guzzlephp.org/en/stable/request-options.html + * + * @throws InvalidApiKeyException + * @throws InvalidArgumentException + */ + protected function __construct(#[SensitiveParameter] string $apiKey, ?string $cachePath = null, ?array $clientOptions = null) + { + if (preg_match('/^[0-9a-fA-F]{32}$/', $apiKey) === 0) { + throw new InvalidApiKeyException('API key typically consists of alpha numeric characters and is 32 chars in length'); + } + + $this->apiKey = $apiKey; + + $clientOptions = self::processClientOptions($clientOptions); + + // To ease unit testing and handle cache... + /** @var array{_mockHandler?: MockHandler} $clientOptions */ + $handlerStack = HandlerStack::create($clientOptions['_mockHandler'] ?? null); + + if (is_dir((string) $cachePath) && is_writable((string) $cachePath)) { + $handlerStack->push(middleware: new CacheMiddleware(cacheStrategy: new PrivateCacheStrategy( + cache: new Psr6CacheStorage(cachePool: new FilesystemAdapter(namespace: 'libIo', defaultLifetime: 300, directory: $cachePath)) + )), name: 'cache'); + } + + unset($clientOptions['_mockHandler']); + + $this->client = new Client(array_merge([ + 'base_uri' => self::API_URL, + 'headers' => ['Accept' => 'application/json',], + 'handler' => $handlerStack, + 'http_errors' => true, + 'timeout' => 10, + 'query' => ['api_key' => $this->apiKey,], + ], $clientOptions)); + } + + /** + * @param array $options An associative array with options to set in the request. + * + * @see https://docs.guzzlephp.org/en/stable/request-options.html + * + * @throws GuzzleException + * @throws ClientException + * @throws RateLimitExceededException + */ + protected function request(string $method, string $endpoint, ?array $options = null): ResponseInterface + { + $endpoint = Utils::normalizeEndpoint($endpoint, self::API_URL); + $method = Utils::normalizeMethod($method); + + $requestOptions = [ + 'query' => [ + 'api_key' => $this->apiKey, + ] + ($options['query'] ?? []), + ]; + + unset($options['query']); + + $options = self::processClientOptions($options); + $requestOptions = array_merge($requestOptions, $options); + + try { + return $this->client->request($method, $endpoint, $requestOptions); + } catch (ClientException $clientException) { + if ($clientException->getResponse()->getStatusCode() === 429) { + throw new RateLimitExceededException($clientException); + } + + throw $clientException; + } + } + + /** + * @param array $clientOptions + * + * @return array{}|array + */ + private static function processClientOptions(?array $clientOptions): array + { + $clientOptions ??= []; + + return array_filter($clientOptions, static fn ($value, string $key): bool => match($key) { + 'base_uri', 'handler', 'http_errors', 'query' => false, // do not override these default options + default => true + }, ARRAY_FILTER_USE_BOTH); + } +} diff --git a/src/Exception/InvalidApiKeyException.php b/src/Exception/InvalidApiKeyException.php new file mode 100644 index 0000000..253915e --- /dev/null +++ b/src/Exception/InvalidApiKeyException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Esi\LibrariesIO\Exception; + +use InvalidArgumentException; + +/** + * InvalidApiKeyException. + */ +final class InvalidApiKeyException extends InvalidArgumentException {} diff --git a/src/Exception/InvalidEndpointException.php b/src/Exception/InvalidEndpointException.php new file mode 100644 index 0000000..25dd907 --- /dev/null +++ b/src/Exception/InvalidEndpointException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Esi\LibrariesIO\Exception; + +use InvalidArgumentException; + +/** + * InvalidEndpointException. + */ +final class InvalidEndpointException extends InvalidArgumentException {} diff --git a/src/Exception/InvalidEndpointOptionsException.php b/src/Exception/InvalidEndpointOptionsException.php new file mode 100644 index 0000000..02c4e27 --- /dev/null +++ b/src/Exception/InvalidEndpointOptionsException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Esi\LibrariesIO\Exception; + +use InvalidArgumentException; + +/** + * InvalidEndpointOptionsException. + */ +final class InvalidEndpointOptionsException extends InvalidArgumentException {} diff --git a/src/Exception/RateLimitExceededException.php b/src/Exception/RateLimitExceededException.php index 7bab803..7612030 100644 --- a/src/Exception/RateLimitExceededException.php +++ b/src/Exception/RateLimitExceededException.php @@ -3,41 +3,46 @@ declare(strict_types=1); /** - * LibrariesIO - A simple API wrapper/client for the Libraries.io API. + * This file is part of Esi\LibrariesIO. * - * @author Eric Sizemore + * (c) 2023-2024 Eric Sizemore * - * @version 1.1.1 - * - * @copyright (C) 2023-2024 Eric Sizemore - * @license The MIT License (MIT) - * - * Copyright (C) 2023-2024 Eric Sizemore . - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. */ namespace Esi\LibrariesIO\Exception; -use InvalidArgumentException; +use GuzzleHttp\Exception\ClientException; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use RuntimeException; + +use function vsprintf; /** * RateLimitExceededException. */ -final class RateLimitExceededException extends InvalidArgumentException {} +final class RateLimitExceededException extends RuntimeException +{ + public function __construct(private readonly ClientException $clientException) + { + $request = $clientException->getRequest(); + + $message = vsprintf( + 'Libraries.io Rate Limit Exceeded. Limit: %s, Remaining: %s, Reset: %s', + [ + $request->getHeaderLine('x-ratelimit-limit'), + $request->getHeaderLine('x-ratelimit-remaining'), + $request->getHeaderLine('x-ratelimit-reset'), + ] + ); + + parent::__construct($message, 0, $clientException); + } + + public function getResponse(): ResponseInterface + { + return $this->clientException->getResponse(); + } +} diff --git a/src/LibrariesIO.php b/src/LibrariesIO.php index 0a34ccb..c2e87ad 100644 --- a/src/LibrariesIO.php +++ b/src/LibrariesIO.php @@ -3,242 +3,68 @@ declare(strict_types=1); /** - * LibrariesIO - A simple API wrapper/client for the Libraries.io API. + * This file is part of Esi\LibrariesIO. * - * @author Eric Sizemore + * (c) 2023-2024 Eric Sizemore * - * @version 1.1.1 - * - * @copyright (C) 2023-2024 Eric Sizemore - * @license The MIT License (MIT) - * - * Copyright (C) 2023-2024 Eric Sizemore . - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. */ namespace Esi\LibrariesIO; +use Esi\LibrariesIO\Exception\InvalidApiKeyException; +use Esi\LibrariesIO\Exception\InvalidEndpointException; use Esi\LibrariesIO\Exception\RateLimitExceededException; use GuzzleHttp\Exception\{ ClientException, GuzzleException }; -use GuzzleHttp\{ - Client, - HandlerStack -}; use InvalidArgumentException; -use JsonException; -use Kevinrob\GuzzleCache\{ - CacheMiddleware, - Storage\Psr6CacheStorage, - Strategy\PrivateCacheStrategy -}; use Psr\Http\Message\ResponseInterface; -use RuntimeException; use SensitiveParameter; -use stdClass; -use Symfony\Component\Cache\Adapter\FilesystemAdapter; - -use function implode; -use function in_array; -use function is_dir; -use function is_writable; -use function json_decode; -use function preg_match; -use function str_contains; - -use const JSON_THROW_ON_ERROR; /** - * Main class. - * * @see \Esi\LibrariesIO\Tests\LibrariesIOTest */ -class LibrariesIO +class LibrariesIO extends AbstractClient { /** - * GuzzleHttp Client. - */ - public ?Client $client = null; - - /** - * Base API endpoint. - * - * @var string - */ - protected const API_URL = 'https://libraries.io/api/'; - - /** - * Libraries.io API key. + * @param null|array $clientOptions * - * @see https://libraries.io/account + * @throws InvalidApiKeyException */ - private ?string $apiKey = null; - - /** - * Path to your cache folder on the file system. - */ - private ?string $cachePath = null; - - /** - * Constructor. - * - * @param string $apiKey Your Libraries.io API Key - * @param ?string $cachePath The path to your cache on the filesystem - */ - public function __construct(#[SensitiveParameter] string $apiKey, ?string $cachePath = null) + public function __construct(#[SensitiveParameter] string $apiKey, ?string $cachePath = null, ?array $clientOptions = null) { - if (preg_match('/^[0-9a-fA-F]{32}$/', $apiKey) === 0) { - throw new InvalidArgumentException('API key appears to be invalid, keys are typically alpha numeric and 32 chars in length'); - } - - $this->apiKey = $apiKey; - - if (is_dir((string) $cachePath) && is_writable((string) $cachePath)) { - $this->cachePath = $cachePath; - } + parent::__construct($apiKey, $cachePath, $clientOptions); } /** - * Builds our GuzzleHttp client. - * - * @param array $query + * @throws ClientException + * @throws GuzzleException + * @throws RateLimitExceededException */ - private function makeClient(?array $query = null): Client - { - // From the test suite/PHPUnit? - if ($this->client instanceof Client && $this->apiKey === '098f6bcd4621d373cade4e832627b4f6') { - return $this->client; - } - - //@codeCoverageIgnoreStart - // Some endpoints do not require any query parameters - if ($query === null) { - $query = []; - } - - // Add the API key to the query - $query['api_key'] = $this->apiKey; - - // Client options - $options = [ - 'base_uri' => self::API_URL, - 'query' => $query, - 'headers' => [ - 'Accept' => 'application/json', - ], - 'http_errors' => true, - ]; - - // If we have a cache path, create our Cache handler - if ($this->cachePath !== null) { - // Create default HandlerStack - $stack = HandlerStack::create(); - - // Add this middleware to the top with `push` - $stack->push(new CacheMiddleware(new PrivateCacheStrategy( - new Psr6CacheStorage(new FilesystemAdapter('', 300, $this->cachePath)) - )), 'cache'); - - // Add handler to $options - $options += ['handler' => $stack]; - } - - // Build client - $this->client = new Client($options); - - return $this->client; - //@codeCoverageIgnoreEnd - } - - /** - * Performs the actual client request. - * - * @throws ClientException|GuzzleException|RateLimitExceededException|RuntimeException - */ - private function makeRequest(string $endpoint, string $method = 'GET'): ResponseInterface - { - // Attempt the request - try { - $method = strtoupper($method); - - $request = match($method) { - 'GET', 'POST', 'PUT', 'DELETE' => $this->client?->request($method, $endpoint), - default => $this->client?->request('GET', $endpoint) - }; - - // Shouldn't happen... - if (!$request instanceof ResponseInterface) { - //@codeCoverageIgnoreStart - throw new RuntimeException('$this->client does not appear to be a valid \GuzzleHttp\Client instance'); - //@codeCoverageIgnoreEnd - } - - return $request; - } catch (ClientException $clientException) { - if ($clientException->getResponse()->getStatusCode() === 429) { - throw new RateLimitExceededException('Libraries.io API rate limit exceeded.', previous: $clientException); - } - - throw $clientException; - } - } - - /** - * Performs a request to the 'platforms' endpoint. - * - * @throws InvalidArgumentException|ClientException|GuzzleException|RateLimitExceededException - */ - public function platform(string $endpoint = 'platforms'): ResponseInterface + public function platform(): ResponseInterface { // The only valid endpoint is 'platforms' currently - if ($endpoint !== 'platforms') { - throw new InvalidArgumentException('Invalid endpoint specified. Must be one of: platforms'); - } - - // Build query - $this->makeClient(); - - return $this->makeRequest($endpoint); + return $this->request('GET', 'platforms'); } /** * Performs a request to the 'project' endpoint and a subset endpoint, which can be: * contributors, dependencies, dependent_repositories, dependents, search, sourcerank, or project * - * @param array $options + * @param array|array $options * - * @throws InvalidArgumentException|ClientException|GuzzleException|RateLimitExceededException + * @throws InvalidEndpointException + * @throws ClientException + * @throws GuzzleException + * @throws RateLimitExceededException */ public function project(string $endpoint, array $options): ResponseInterface { - // Make sure we have the format and options for $endpoint - $endpointParameters = self::endpointParameters('project', $endpoint); + [$endpointFormat, $endpointOptions, $requestMethod] = Utils::endpointParameters('project', $endpoint, $options); - /** @var array $endpointOptions * */ - $endpointOptions = $endpointParameters['options']; - - self::verifyEndpointOptions($endpointOptions, $options); - - // Build query $query = [ 'page' => $options['page'] ?? 1, 'per_page' => $options['per_page'] ?? 30, @@ -248,290 +74,90 @@ public function project(string $endpoint, array $options): ResponseInterface if ($endpoint === 'search') { $query += [ 'q' => $options['query'], - 'sort' => self::searchVerifySortOption(/** @phpstan-ignore-line **/$options['sort']), + 'sort' => Utils::searchVerifySortOption((string) $options['sort']), ]; - // Search can also have: 'languages', 'licenses', 'keywords', 'platforms' as additional parameters - $additionalParams = self::searchAdditionalParams($options); + $additionalParams = Utils::searchAdditionalParams($options); if ($additionalParams !== []) { $query += $additionalParams; } } - // Build the client - $this->makeClient($query); - - // Attempt the request - $endpointParameters['format'] = self::processEndpointFormat(/** @phpstan-ignore-line **/$endpointParameters['format'], $options); + $query = ['query' => $query]; - return $this->makeRequest($endpointParameters['format']); + return $this->request($requestMethod, $endpointFormat, $query); } /** * Performs a request to the 'repository' endpoint and a subset endpoint, which can be: * dependencies, projects, or repository * - * @param array $options + * @param array|array $options * - * @throws InvalidArgumentException|ClientException|GuzzleException|RateLimitExceededException + * @throws InvalidEndpointException + * @throws ClientException + * @throws GuzzleException + * @throws RateLimitExceededException */ public function repository(string $endpoint, array $options): ResponseInterface { - // Make sure we have the format and options for $endpoint - $endpointParameters = self::endpointParameters('repository', $endpoint); - - /** @var array $endpointOptions * */ - $endpointOptions = $endpointParameters['options']; - - self::verifyEndpointOptions($endpointOptions, $options); - - // Build query - $this->makeClient([ - // Using pagination? - 'page' => $options['page'] ?? 1, - 'per_page' => $options['per_page'] ?? 30, - ]); - - // Attempt the request - $endpointParameters['format'] = self::processEndpointFormat(/** @phpstan-ignore-line **/$endpointParameters['format'], $options); - - return $this->makeRequest($endpointParameters['format']); - } - - /** - * Performs a request to the 'user' endpoint and a subset endpoint, which can be: - * dependencies, package_contributions, packages, repositories, repository_contributions, or subscriptions - * - * @param array $options - * - * @throws InvalidArgumentException|ClientException|GuzzleException|RateLimitExceededException - */ - public function user(string $endpoint, array $options): ResponseInterface - { - // Make sure we have the format and options for $endpoint - $endpointParameters = self::endpointParameters('user', $endpoint); - - /** @var array $endpointOptions * */ - $endpointOptions = $endpointParameters['options']; + [$endpointFormat, $endpointOptions, $requestMethod] = Utils::endpointParameters('repository', $endpoint, $options); - self::verifyEndpointOptions($endpointOptions, $options); - - // Build query - $this->makeClient([ - 'page' => $options['page'] ?? 1, - 'per_page' => $options['per_page'] ?? 30, - ]); - - // Attempt the request - $endpointParameters['format'] = self::processEndpointFormat(/** @phpstan-ignore-line **/$endpointParameters['format'], $options); + $query = [ + 'query' => [ + 'page' => $options['page'] ?? 1, + 'per_page' => $options['per_page'] ?? 30, + ], + ]; - return $this->makeRequest($endpointParameters['format']); + return $this->request($requestMethod, $endpointFormat, $query); } /** * Performs a request to the 'subscription' endpoint and a subset endpoint, which can be: * subscribe, check, update, unsubscribe * - * @param array $options + * @param array $options * - * @throws InvalidArgumentException|ClientException|GuzzleException|RateLimitExceededException + * @throws InvalidEndpointException + * @throws ClientException + * @throws GuzzleException + * @throws RateLimitExceededException */ public function subscription(string $endpoint, array $options): ResponseInterface { - // Make sure we have the format and options for $endpoint - $endpointParameters = self::endpointParameters('subscription', $endpoint); - - /** @var array $endpointOptions * */ - $endpointOptions = $endpointParameters['options']; - - self::verifyEndpointOptions($endpointOptions, $options); + [$endpointFormat, $endpointOptions, $requestMethod] = Utils::endpointParameters('subscription', $endpoint, $options); - // Build query if (isset($options['include_prerelease'])) { - $query = ['include_prerelease' => $options['include_prerelease']]; + $query = ['query' => ['include_prerelease' => $options['include_prerelease']]]; } - $this->makeClient($query ?? []); - - // Attempt the request - $endpointParameters['format'] = self::processEndpointFormat(/** @phpstan-ignore-line **/$endpointParameters['format'], $options); - - return $this->makeRequest($endpointParameters['format'], /** @phpstan-ignore-line **/$endpointParameters['method']); + return $this->request($requestMethod, $endpointFormat, $query ?? []); } /** - * Processes the available parameters for a given endpoint. - * - * @return array|string> - * - * @throws InvalidArgumentException - */ - private static function endpointParameters(string $endpoint, string $subset): array - { - static $projectParameters = [ - 'contributors' => ['format' => ':platform/:name/contributors', 'options' => ['platform', 'name']], - 'dependencies' => ['format' => ':platform/:name/:version/dependencies', 'options' => ['platform', 'name', 'version']], - 'dependent_repositories' => ['format' => ':platform/:name/dependent_repositories', 'options' => ['platform', 'name']], - 'dependents' => ['format' => ':platform/:name/dependents', 'options' => ['platform', 'name']], - 'search' => ['format' => 'search', 'options' => ['query', 'sort']], - 'sourcerank' => ['format' => ':platform/:name/sourcerank', 'options' => ['platform', 'name']], - 'project' => ['format' => ':platform/:name', 'options' => ['platform', 'name']], - ]; - - static $repositoryParameters = [ - 'dependencies' => ['format' => 'github/:owner/:name/dependencies', 'options' => ['owner', 'name']], - 'projects' => ['format' => 'github/:owner/:name/projects', 'options' => ['owner', 'name']], - 'repository' => ['format' => 'github/:owner/:name', 'options' => ['owner', 'name']], - ]; - - static $userParameters = [ - 'dependencies' => ['format' => 'github/:login/dependencies', 'options' => ['login']], - 'package_contributions' => ['format' => 'github/:login/project-contributions', 'options' => ['login']], - 'packages' => ['format' => 'github/:login/projects', 'options' => ['login']], - 'repositories' => ['format' => 'github/:login/repositories', 'options' => ['login']], - 'repository_contributions' => ['format' => 'github/:login/repository-contributions', 'options' => ['login']], - 'subscriptions' => ['format' => 'subscriptions', 'options' => []], - 'user' => ['format' => 'github/:login', 'options' => ['login']], - ]; - - static $subscriptionParameters = [ - 'subscribe' => ['format' => 'subscriptions/:platform/:name', 'options' => ['platform', 'name', 'include_prerelease'], 'method' => 'post'], - 'check' => ['format' => 'subscriptions/:platform/:name', 'options' => ['platform', 'name'], 'method' => 'get'], - 'update' => ['format' => 'subscriptions/:platform/:name', 'options' => ['platform', 'name', 'include_prerelease'], 'method' => 'put'], - 'unsubscribe' => ['format' => 'subscriptions/:platform/:name', 'options' => ['platform', 'name'], 'method' => 'delete'], - ]; - - return match($endpoint) { - 'project' => $projectParameters[$subset] ?? throw new InvalidArgumentException('Invalid endpoint subset specified.'), - 'repository' => $repositoryParameters[$subset] ?? throw new InvalidArgumentException('Invalid endpoint subset specified.'), - 'user' => $userParameters[$subset] ?? throw new InvalidArgumentException('Invalid endpoint subset specified.'), - 'subscription' => $subscriptionParameters[$subset] ?? throw new InvalidArgumentException('Invalid endpoint subset specified.'), - default => throw new InvalidArgumentException('Invalid endpoint subset specified.') - }; - } - - /** - * Each endpoint class will have a 'subset' of endpoints that fall under it. This - * function handles returning a formatted endpoint for the Client. - * - * @param array $options - */ - private static function processEndpointFormat(string $format, array $options): string - { - if (str_contains($format, ':') === false) { - return $format; - } - - foreach ($options as $key => $val) { - if (in_array($key, ['page', 'per_page'], true)) { - continue; - } - - /** @var string $val * */ - $format = str_replace(':' . $key, $val, $format); - } - - return $format; - } - - /** - * Helper function to make sure that the $options passed makeRequest() - * contains the required options listed in the endpoints options. - * - * @param array $endpointOptions - * @param array $options - * - * @throws InvalidArgumentException - */ - private static function verifyEndpointOptions(array $endpointOptions, array $options): void - { - foreach ($endpointOptions as $endpointOption) { - if (!isset($options[$endpointOption])) { - throw new InvalidArgumentException( - '$options has not specified all required parameters. Parameters needed: ' . implode(', ', $endpointOptions) - ); - } - } - } - - /** - * Processes the additional parameters that can be used by the search endpoint. + * Performs a request to the 'user' endpoint and a subset endpoint, which can be: + * dependencies, package_contributions, packages, repositories, repository_contributions, or subscriptions * - * @param array $options + * @param array|array $options * - * @return array + * @throws InvalidEndpointException + * @throws ClientException + * @throws GuzzleException + * @throws RateLimitExceededException */ - private static function searchAdditionalParams(array $options): array + public function user(string $endpoint, array $options): ResponseInterface { - $additionalParams = []; + [$endpointFormat, $endpointOptions, $requestMethod] = Utils::endpointParameters('user', $endpoint, $options); - foreach (['languages', 'licenses', 'keywords', 'platforms'] as $option) { - if (isset($options[$option])) { - $additionalParams[$option] = $options[$option]; - } - } - - return $additionalParams; - } - - /** - * Verifies that the provided sort option is a valid one that libraries.io's API supports. - */ - private static function searchVerifySortOption(string $sort): string - { - static $sortOptions = [ - 'rank', 'stars', 'dependents_count', - 'dependent_repos_count', 'latest_release_published_at', - 'contributions_count', 'created_at', + $query = [ + 'query' => [ + 'page' => $options['page'] ?? 1, + 'per_page' => $options['per_page'] ?? 30, + ], ]; - if (!in_array($sort, $sortOptions, true)) { - return 'rank'; - } - - return $sort; - } - - /** - * Returns the jSON data as-is from the API. - * - * @param ResponseInterface $response The response object from makeRequest() - */ - public function raw(ResponseInterface $response): string - { - return $response->getBody()->getContents(); - } - - /** - * Decodes the jSON returned from the API. Returns as an associative array. - * - * @param ResponseInterface $response The response object from makeRequest() - * - * @return array - * - * @throws JsonException - */ - public function toArray(ResponseInterface $response): array - { - /** @var array $json * */ - $json = json_decode($this->raw($response), true, flags: JSON_THROW_ON_ERROR); - - return $json; - } - - /** - * Decodes the jSON returned from the API. Returns as an array of objects. - * - * @param ResponseInterface $response The response object from makeRequest() - * - * @throws JsonException - */ - public function toObject(ResponseInterface $response): stdClass - { - /** @var stdClass $json * */ - $json = json_decode($this->raw($response), false, flags: JSON_THROW_ON_ERROR); - - return $json; + return $this->request($requestMethod, $endpointFormat, $query); } } diff --git a/src/Utils.php b/src/Utils.php new file mode 100644 index 0000000..a4b885d --- /dev/null +++ b/src/Utils.php @@ -0,0 +1,225 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Esi\LibrariesIO; + +use Esi\LibrariesIO\Exception\InvalidEndpointException; +use Esi\LibrariesIO\Exception\InvalidEndpointOptionsException; +use InvalidArgumentException; +use JsonException; +use Psr\Http\Message\ResponseInterface; +use stdClass; + +use function implode; +use function in_array; +use function json_decode; +use function ltrim; +use function str_contains; +use function str_ends_with; +use function str_replace; +use function strtoupper; + +use const JSON_THROW_ON_ERROR; + +/** + * Utility class. + */ +final class Utils +{ + /** + * @var array, method: string}> + */ + public static array $projectParameters = [ + 'contributors' => ['format' => ':platform/:name/contributors', 'options' => ['platform', 'name'], 'method' => 'get'], + 'dependencies' => ['format' => ':platform/:name/:version/dependencies', 'options' => ['platform', 'name', 'version'], 'method' => 'get'], + 'dependent_repositories' => ['format' => ':platform/:name/dependent_repositories', 'options' => ['platform', 'name'], 'method' => 'get'], + 'dependents' => ['format' => ':platform/:name/dependents', 'options' => ['platform', 'name'], 'method' => 'get'], + 'search' => ['format' => 'search', 'options' => ['query', 'sort'], 'method' => 'get'], + 'sourcerank' => ['format' => ':platform/:name/sourcerank', 'options' => ['platform', 'name'], 'method' => 'get'], + 'project' => ['format' => ':platform/:name', 'options' => ['platform', 'name'], 'method' => 'get'], + ]; + + /** + * @var array, method: string}> + */ + public static array $repositoryParameters = [ + 'dependencies' => ['format' => 'github/:owner/:name/dependencies', 'options' => ['owner', 'name'], 'method' => 'get'], + 'projects' => ['format' => 'github/:owner/:name/projects', 'options' => ['owner', 'name'], 'method' => 'get'], + 'repository' => ['format' => 'github/:owner/:name', 'options' => ['owner', 'name'], 'method' => 'get'], + ]; + + /** + * @var array, method: string}> + */ + public static array $subscriptionParameters = [ + 'subscribe' => ['format' => 'subscriptions/:platform/:name', 'options' => ['platform', 'name', 'include_prerelease'], 'method' => 'post'], + 'check' => ['format' => 'subscriptions/:platform/:name', 'options' => ['platform', 'name'], 'method' => 'get'], + 'update' => ['format' => 'subscriptions/:platform/:name', 'options' => ['platform', 'name', 'include_prerelease'], 'method' => 'put'], + 'unsubscribe' => ['format' => 'subscriptions/:platform/:name', 'options' => ['platform', 'name'], 'method' => 'delete'], + ]; + + /** + * @var array, method: string}> + */ + public static array $userParameters = [ + 'dependencies' => ['format' => 'github/:login/dependencies', 'options' => ['login'], 'method' => 'get'], + 'package_contributions' => ['format' => 'github/:login/project-contributions', 'options' => ['login'], 'method' => 'get'], + 'packages' => ['format' => 'github/:login/projects', 'options' => ['login'], 'method' => 'get'], + 'repositories' => ['format' => 'github/:login/repositories', 'options' => ['login'], 'method' => 'get'], + 'repository_contributions' => ['format' => 'github/:login/repository-contributions', 'options' => ['login'], 'method' => 'get'], + 'subscriptions' => ['format' => 'subscriptions', 'options' => [], 'method' => 'get'], + 'user' => ['format' => 'github/:login', 'options' => ['login'], 'method' => 'get'], + ]; + + /** + * @param array $options + * + * @return array{0: string, 1: array{}|array, 2: string} + * + * @throws InvalidEndpointException + * @throws InvalidEndpointOptionsException + */ + public static function endpointParameters(string $endpoint, string $subset, array $options): array + { + $parameters = match($endpoint) { + 'project' => Utils::$projectParameters[$subset] ?? throw new InvalidEndpointException('Invalid endpoint subset specified.'), + 'repository' => Utils::$repositoryParameters[$subset] ?? throw new InvalidEndpointException('Invalid endpoint subset specified.'), + 'user' => Utils::$userParameters[$subset] ?? throw new InvalidEndpointException('Invalid endpoint subset specified.'), + 'subscription' => Utils::$subscriptionParameters[$subset] ?? throw new InvalidEndpointException('Invalid endpoint subset specified.'), + default => throw new InvalidEndpointException('Invalid endpoint subset specified.') + }; + + foreach ($parameters['options'] as $endpointOption) { + if (!isset($options[$endpointOption])) { + throw new InvalidEndpointOptionsException( + '$options has not specified all required parameters. Parameters needed: ' . implode(', ', $parameters['options']) + ); + } + } + + $parameters['format'] = Utils::processEndpointFormat($parameters['format'], $options); + + return [$parameters['format'], $parameters['options'], $parameters['method']]; + } + + public static function normalizeEndpoint(string | null $endpoint, string $apiUrl): string + { + $endpoint = ltrim((string) $endpoint, '/'); + + if (!str_ends_with($apiUrl, '/')) { + $endpoint = '/' . $endpoint; + } + + return $endpoint; + } + + public static function normalizeMethod(string $method): string + { + static $availableMethods = ['GET', 'DELETE', 'POST', 'PUT',]; + + $method = strtoupper($method); + + // Check for a valid method + if (!in_array($method, $availableMethods, true)) { + $method = 'GET'; + } + + return $method; + } + + public static function raw(ResponseInterface $response): string + { + return $response->getBody()->getContents(); + } + + /** + * @param array $options + * + * @return array + */ + public static function searchAdditionalParams(array $options): array + { + $additionalParams = []; + + foreach (['languages', 'licenses', 'keywords', 'platforms'] as $option) { + if (isset($options[$option])) { + $additionalParams[$option] = $options[$option]; + } + } + + return $additionalParams; + } + + public static function searchVerifySortOption(string $sort): string + { + static $sortOptions = [ + 'rank', 'stars', 'dependents_count', + 'dependent_repos_count', 'latest_release_published_at', + 'contributions_count', 'created_at', + ]; + + if (!in_array($sort, $sortOptions, true)) { + return 'rank'; + } + + return $sort; + } + + /** + * @return array + * + * @throws JsonException + */ + public static function toArray(ResponseInterface $response): array + { + /** @var array $json * */ + $json = json_decode(Utils::raw($response), true, flags: JSON_THROW_ON_ERROR); + + return $json; + } + + /** + * @throws JsonException + */ + public static function toObject(ResponseInterface $response): stdClass + { + /** @var stdClass $json * */ + $json = json_decode(Utils::raw($response), false, flags: JSON_THROW_ON_ERROR); + + return $json; + } + + /** + * Each endpoint class will have a 'subset' of endpoints that fall under it. This + * function handles returning a formatted endpoint for the Client. + * + * @param array $options + */ + private static function processEndpointFormat(string $format, array $options): string + { + if (str_contains($format, ':') === false) { + return $format; + } + + foreach ($options as $key => $val) { + if (in_array($key, ['page', 'per_page'], true)) { + continue; + } + + /** @var string $val * */ + $format = str_replace(':' . $key, $val, $format); + } + + return $format; + } +} diff --git a/tests/src/LibrariesIOTest.php b/tests/src/LibrariesIOTest.php index ab750b2..b5cd15f 100644 --- a/tests/src/LibrariesIOTest.php +++ b/tests/src/LibrariesIOTest.php @@ -3,42 +3,24 @@ declare(strict_types=1); /** - * LibrariesIO - A simple API wrapper/client for the Libraries.io API. + * This file is part of Esi\LibrariesIO. * - * @author Eric Sizemore + * (c) 2023-2024 Eric Sizemore * - * @version 1.1.1 - * - * @copyright (C) 2023-2024 Eric Sizemore - * @license The MIT License (MIT) - * - * Copyright (C) 2023-2024 Eric Sizemore . - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. */ namespace Esi\LibrariesIO\Tests; +use Esi\LibrariesIO\AbstractClient; +use Esi\LibrariesIO\Exception\InvalidApiKeyException; +use Esi\LibrariesIO\Exception\InvalidEndpointException; +use Esi\LibrariesIO\Exception\InvalidEndpointOptionsException; use Esi\LibrariesIO\Exception\RateLimitExceededException; use Esi\LibrariesIO\LibrariesIO; +use Esi\LibrariesIO\Utils; use GuzzleHttp\{ - Client, Exception\ClientException, HandlerStack, Handler\MockHandler, @@ -50,6 +32,7 @@ use PHPUnit\Framework\{ Attributes\CoversClass, Attributes\DataProvider, + Attributes\TestDox, MockObject\MockObject, TestCase }; @@ -63,194 +46,227 @@ * @internal */ #[CoversClass(LibrariesIO::class)] +#[CoversClass(AbstractClient::class)] +#[CoversClass(Utils::class)] +#[CoversClass(RateLimitExceededException::class)] final class LibrariesIOTest extends TestCase { /** - * A mock'ed GuzzleHttp client we can inject for testing. + * @var array */ - protected Client $client; + private array $responses; - /** - * The mock/stub of the main class. - */ - protected LibrariesIO&MockObject $stub; + private string $testApiKey; - /** - * Creates the mock to be used throughout testing. - */ #[\Override] protected function setUp(): void { - // Create a mock and queue two responses. - $mockHandler = new MockHandler([ - new Response(200, body: '{"Hello":"World"}'), - ]); - - $handlerStack = HandlerStack::create($mockHandler); - $this->client = new Client(['handler' => $handlerStack]); + $rateLimitHeaders = [ + 'X-RateLimit-Limit' => '60', + 'X-RateLimit-Remaining' => '0', + 'X-RateLimit-Reset' => '', + ]; + + $this->testApiKey = md5('test'); + + $this->responses = [ + 'valid' => new Response(200, body: '{"Hello":"World"}'), + 'clientError' => new ClientException('Error Communicating with Server', new Request('GET', 'test'), new Response(202, ['X-Foo' => 'Bar'])), + 'rateLimit' => new ClientException('Rate Limit Exceeded', new Request('GET', 'test'), new Response(429, $rateLimitHeaders)), + 'rateLimiter' => new Response(429, $rateLimitHeaders, 'Rate Limit Exceeded'), + ]; + } - $this->stub = $this - ->getMockBuilder(LibrariesIO::class) - ->setConstructorArgs([md5('test'), sys_get_temp_dir()]) - ->onlyMethods([]) - ->getMock(); + public static function dataEndpointProvider(): Iterator + { + yield ['project', '/project', 'https://libraries.io/api/']; + yield ['project', 'project', 'https://libraries.io/api/']; + yield ['/project', '/project', 'https://libraries.io/api']; + yield ['/project', 'project', 'https://libraries.io/api']; } - /** - * Mock a client error via Guzzle's ClientException. - */ - public function testClientError(): void + public static function dataMethodProvider(): Iterator { - // Create a mock and queue two responses. - $mockHandler = new MockHandler([ - new ClientException('Error Communicating with Server', new Request('GET', 'test'), new Response(202, ['X-Foo' => 'Bar'])), - ]); + yield ['GET', 'GET']; + yield ['POST', 'POST']; + yield ['DELETE', 'DELETE']; + yield ['PUT', 'PUT']; + yield ['GET', 'PATCH']; + yield ['GET', 'OPTIONS']; + yield ['GET', 'HEAD']; + } - $handlerStack = HandlerStack::create($mockHandler); - $client = new Client(['handler' => $handlerStack]); + public static function dataProjectProvider(): Iterator + { + yield ['{"Hello":"World"}', 'contributors', ['platform' => 'npm', 'name' => 'utility']]; + yield ['{"Hello":"World"}', 'dependencies', ['platform' => 'npm', 'name' => 'utility', 'version' => 'latest']]; + yield ['{"Hello":"World"}', 'dependent_repositories', ['platform' => 'npm', 'name' => 'utility']]; + yield ['{"Hello":"World"}', 'dependents', ['platform' => 'npm', 'name' => 'utility']]; + yield ['{"Hello":"World"}', 'search', ['query' => 'grunt', 'sort' => 'rank', 'keywords' => 'wordpress']]; + yield ['{"Hello":"World"}', 'search', ['query' => 'grunt', 'sort' => 'notvalid', 'keywords' => 'wordpress']]; + yield ['{"Hello":"World"}', 'sourcerank', ['platform' => 'npm', 'name' => 'utility']]; + yield ['{"Hello":"World"}', 'project', ['platform' => 'npm', 'name' => 'utility', 'page' => 1, 'per_page' => 30]]; + } - $stub = $this - ->getMockBuilder(LibrariesIO::class) - ->setConstructorArgs([md5('test'), sys_get_temp_dir()]) - ->onlyMethods([]) - ->getMock(); - $this->expectException(ClientException::class); - $stub->client = $client; - $stub->platform(); + public static function dataRepositoryProvider(): Iterator + { + yield ['{"Hello":"World"}', 'dependencies', ['owner' => 'ericsizemore', 'name' => 'utility']]; + yield ['{"Hello":"World"}', 'projects', ['owner' => 'ericsizemore', 'name' => 'utility']]; + yield ['{"Hello":"World"}', 'repository', ['owner' => 'ericsizemore', 'name' => 'utility']]; + yield ['{"Hello":"World"}', 'dependencies', ['owner' => 'ericsizemore', 'name' => 'utility']]; + yield ['{"Hello":"World"}', 'projects', ['owner' => 'ericsizemore', 'name' => 'utility', 'page' => 1, 'per_page' => 30]]; + yield ['{"Hello":"World"}', 'repository', ['owner' => 'ericsizemore', 'name' => 'utility']]; } - /** - * Tests library handling of HTTP 429, which can be returned by libraries.io if rate limit - * is exceeded. - */ - public function testRateLimitExceeded(): void + public static function dataSubscriptionProvider(): Iterator { - // Create a mock and queue two responses. - $mockHandler = new MockHandler([ - new ClientException('Error Communicating with Server', new Request('GET', 'test'), new Response(429, ['X-Foo' => 'Bar'])), - ]); + yield ['{"Hello":"World"}', 'subscribe', ['platform' => 'npm', 'name' => 'utility', 'include_prerelease' => 'true']]; + yield ['{"Hello":"World"}', 'check', ['platform' => 'npm', 'name' => 'utility']]; + yield ['{"Hello":"World"}', 'update', ['platform' => 'npm', 'name' => 'utility', 'include_prerelease' => 'false']]; + yield ['{"Hello":"World"}', 'unsubscribe', ['platform' => 'npm', 'name' => 'utility']]; + } - $handlerStack = HandlerStack::create($mockHandler); - $client = new Client(['handler' => $handlerStack]); + public static function dataUserProvider(): Iterator + { + yield ['{"Hello":"World"}', 'dependencies', ['login' => 'ericsizemore']]; + yield ['{"Hello":"World"}', 'package_contributions', ['login' => 'ericsizemore']]; + yield ['{"Hello":"World"}', 'packages', ['login' => 'ericsizemore']]; + yield ['{"Hello":"World"}', 'repositories', ['login' => 'ericsizemore']]; + yield ['{"Hello":"World"}', 'repository_contributions', ['login' => 'ericsizemore', 'page' => 1, 'per_page' => 30]]; + yield ['{"Hello":"World"}', 'subscriptions', []]; + } - $stub = $this - ->getMockBuilder(LibrariesIO::class) - ->setConstructorArgs([md5('test'), sys_get_temp_dir()]) - ->onlyMethods([]) - ->getMock(); - $this->expectException(RateLimitExceededException::class); - $stub->client = $client; - $stub->platform(); + #[TestDox('Client error throws a Guzzle ClientException')] + public function testClientError(): void + { + $mockHandler = new MockHandler([$this->responses['clientError']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $this->expectException(ClientException::class); + $mockClient->platform(); } - /** - * Test providing an invalid API key. - */ + #[TestDox('Providing an invalid API key results in an InvalidApiKeyException')] public function testInvalidApiKey(): void { - $this->expectException(InvalidArgumentException::class); - $this - ->getMockBuilder(LibrariesIO::class) - ->setConstructorArgs(['notvalid', sys_get_temp_dir()]) - ->onlyMethods([]) - ->getMock(); + $this->expectException(InvalidApiKeyException::class); + $this->mockClient('notvalid'); } - /** - * Test the platform endpoint. - */ - public function testPlatform(): void + #[DataProvider('dataEndpointProvider')] + public function testNormalizeEndpoint(string $expected, string $endpoint, string $apiUrl): void { - $this->stub->client = $this->client; - $response = $this->stub->platform(); - - self::assertInstanceOf(Response::class, $response); - self::assertSame('{"Hello":"World"}', $response->getBody()->getContents()); + self::assertSame($expected, Utils::normalizeEndpoint($endpoint, $apiUrl)); } - /** - * Test the platform endpoint with an invalid $endpoint arg specified. - */ - public function testPlatformInvalid(): void + #[DataProvider('dataMethodProvider')] + public function testNormalizeMethod(string $expected, string $method): void { - $this->stub->client = $this->client; - $this->expectException(InvalidArgumentException::class); - $this->stub->platform('notvalid'); + self::assertSame($expected, Utils::normalizeMethod($method)); } - /** - * Provides testing data for the project endpoint testing. - */ - public static function dataProjectProvider(): Iterator + #[TestDox('LibrariesIO::platform() returns expected response')] + public function testPlatform(): void { - yield ['{"Hello":"World"}', 'contributors', ['platform' => 'npm', 'name' => 'utility']]; - yield ['{"Hello":"World"}', 'dependencies', ['platform' => 'npm', 'name' => 'utility', 'version' => 'latest']]; - yield ['{"Hello":"World"}', 'dependent_repositories', ['platform' => 'npm', 'name' => 'utility']]; - yield ['{"Hello":"World"}', 'dependents', ['platform' => 'npm', 'name' => 'utility']]; - yield ['{"Hello":"World"}', 'search', ['query' => 'grunt', 'sort' => 'rank', 'keywords' => 'wordpress']]; - yield ['{"Hello":"World"}', 'search', ['query' => 'grunt', 'sort' => 'notvalid', 'keywords' => 'wordpress']]; - yield ['{"Hello":"World"}', 'sourcerank', ['platform' => 'npm', 'name' => 'utility']]; - yield ['{"Hello":"World"}', 'project', ['platform' => 'npm', 'name' => 'utility', 'page' => 1, 'per_page' => 30]]; + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $response = $mockClient->platform(); + + self::assertInstanceOf(Response::class, $response); + self::assertSame('{"Hello":"World"}', $response->getBody()->getContents()); } /** - * Tests the project endpoint. - * * @param array $options */ #[DataProvider('dataProjectProvider')] + #[TestDox('LibrariesIO::project() returns expected response')] public function testProject(string $expected, string $endpoint, array $options): void { - $this->stub->client = $this->client; - $response = $this->stub->project($endpoint, $options); + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $response = $mockClient->project($endpoint, $options); self::assertInstanceOf(Response::class, $response); self::assertSame($expected, $response->getBody()->getContents()); } - /** - * Test the project endpoint with an invalid subset $endpoint arg specified. - */ + #[TestDox('LibrariesIO::project() with an invalid endpoint throws an InvalidEndpointException')] public function testProjectInvalidEndpoint(): void { - $this->stub->client = $this->client; - $this->expectException(InvalidArgumentException::class); - $this->stub->project('notvalid', ['platform' => 'npm', 'name' => 'utility']); + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + + $this->expectException(InvalidEndpointException::class); + $mockClient->project('notvalid', ['platform' => 'npm', 'name' => 'utility']); } - /** - * Test the platform endpoint with n valid subset $endpoint arg and invalid $options specified. - */ + #[TestDox('LibrariesIO::project() with an invalid options throws an InvalidEndpointOptionsException')] public function testProjectInvalidOptions(): void { - $this->stub->client = $this->client; - $this->expectException(InvalidArgumentException::class); - $this->stub->project('search', ['huh' => 'what']); + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + + $this->expectException(InvalidEndpointOptionsException::class); + $mockClient->project('search', ['huh' => 'what']); } - /** - * Provides testing data for the repository endpoint. - */ - public static function dataRepositoryProvider(): Iterator + #[TestDox('A response with status code 429 throws a RateLimitExceededException')] + public function testRateLimitExceeded(): void { - yield ['{"Hello":"World"}', 'dependencies', ['owner' => 'ericsizemore', 'name' => 'utility']]; - yield ['{"Hello":"World"}', 'projects', ['owner' => 'ericsizemore', 'name' => 'utility']]; - yield ['{"Hello":"World"}', 'repository', ['owner' => 'ericsizemore', 'name' => 'utility']]; - yield ['{"Hello":"World"}', 'dependencies', ['owner' => 'ericsizemore', 'name' => 'utility']]; - yield ['{"Hello":"World"}', 'projects', ['owner' => 'ericsizemore', 'name' => 'utility', 'page' => 1, 'per_page' => 30]]; - yield ['{"Hello":"World"}', 'repository', ['owner' => 'ericsizemore', 'name' => 'utility']]; + $response = $this->responses['rateLimiter']; + $mockHandler = new MockHandler([$response]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + + try { + $mockClient->platform(); + } catch (RateLimitExceededException $rateLimitExceededException) { + $response = $rateLimitExceededException->getResponse(); + + self::assertSame('60', $response->getHeaderLine('x-ratelimit-limit')); + self::assertSame('0', $response->getHeaderLine('x-ratelimit-remaining')); + self::assertSame('', $response->getHeaderLine('x-ratelimit-reset')); + } + } + + #[TestDox('(ClientException) A response with status code 429 throws a RateLimitExceededException')] + public function testRateLimitExceededClientException(): void + { + $response = $this->responses['rateLimit']; + $mockHandler = new MockHandler([$response]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + + try { + $mockClient->platform(); + } catch (RateLimitExceededException $rateLimitExceededException) { + $response = $rateLimitExceededException->getResponse(); + + self::assertSame('60', $response->getHeaderLine('x-ratelimit-limit')); + self::assertSame('0', $response->getHeaderLine('x-ratelimit-remaining')); + self::assertSame('', $response->getHeaderLine('x-ratelimit-reset')); + } + } + + #[TestDox('Utils::toRaw() returns raw JSON and expected response')] + public function testRaw(): void + { + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $response = $mockClient->user('dependencies', ['login' => 'ericsizemore']); + + self::assertInstanceOf(Response::class, $response); + self::assertSame('{"Hello":"World"}', Utils::raw($response)); } /** - * Test the repository endpoint. - * * @param array $options */ #[DataProvider('dataRepositoryProvider')] + #[TestDox('LibrariesIO::repository() returns expected response')] public function testRepository(string $expected, string $endpoint, array $options): void { - $this->stub->client = $this->client; - $response = $this->stub->repository($endpoint, $options); + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $response = $mockClient->repository($endpoint, $options); self::assertInstanceOf(Response::class, $response); self::assertSame($expected, $response->getBody()->getContents()); @@ -261,9 +277,10 @@ public function testRepository(string $expected, string $endpoint, array $option */ public function testRepositoryInvalidEndpoint(): void { - $this->stub->client = $this->client; - $this->expectException(InvalidArgumentException::class); - $this->stub->repository('notvalid', ['owner' => 'ericsizemore', 'name' => 'utility']); + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $this->expectException(InvalidEndpointException::class); + $mockClient->repository('notvalid', ['owner' => 'ericsizemore', 'name' => 'utility']); } /** @@ -271,142 +288,130 @@ public function testRepositoryInvalidEndpoint(): void */ public function testRepositoryInvalidOptions(): void { - $this->stub->client = $this->client; + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); $this->expectException(InvalidArgumentException::class); - $this->stub->repository('repository', ['huh' => 'what']); - } - - /** - * Provides the data for testing the user endpoint. - */ - public static function dataUserProvider(): Iterator - { - yield ['{"Hello":"World"}', 'dependencies', ['login' => 'ericsizemore']]; - yield ['{"Hello":"World"}', 'package_contributions', ['login' => 'ericsizemore']]; - yield ['{"Hello":"World"}', 'packages', ['login' => 'ericsizemore']]; - yield ['{"Hello":"World"}', 'repositories', ['login' => 'ericsizemore']]; - yield ['{"Hello":"World"}', 'repository_contributions', ['login' => 'ericsizemore', 'page' => 1, 'per_page' => 30]]; - yield ['{"Hello":"World"}', 'subscriptions', []]; + $mockClient->repository('repository', ['huh' => 'what']); } /** - * Test the user endpoint. + * Test the subscription endpoint. * - * @param array $options + * @param array $options */ - #[DataProvider('dataUserProvider')] - public function testUser(string $expected, string $endpoint, array $options): void + #[DataProvider('dataSubscriptionProvider')] + public function testSubscription(string $expected, string $endpoint, array $options): void { - $this->stub->client = $this->client; - $response = $this->stub->user($endpoint, $options); + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $response = $mockClient->subscription($endpoint, $options); self::assertInstanceOf(Response::class, $response); self::assertSame($expected, $response->getBody()->getContents()); } /** - * Test the user endpoint with an invalid $endpoint arg specified. + * Test the subscription endpoint with an invalid $endpoint arg specified. */ - public function testUserInvalidEndpoint(): void + public function testSubscriptionInvalidEndpoint(): void { - $this->stub->client = $this->client; - $this->expectException(InvalidArgumentException::class); - $this->stub->user('notvalid', ['login' => 'ericsizemore']); + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $this->expectException(InvalidEndpointException::class); + $mockClient->subscription('notvalid', ['platform' => 'npm', 'name' => 'utility']); } /** - * Test the user endpoint with a valid $endpoint arg and invalid $options specified. + * Test the subscription endpoint with a valid $endpoint arg and invalid $options specified. */ - public function testUserInvalidOptions(): void + public function testSubscriptionInvalidOptions(): void { - $this->stub->client = $this->client; - $this->expectException(InvalidArgumentException::class); - $this->stub->user('packages', ['huh' => 'what']); + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $this->expectException(InvalidEndpointOptionsException::class); + $mockClient->subscription('check', ['huh' => 'what']); } /** - * Provides the data for testing the subscription endpoint. + * Test the toArray function. It decodes the raw json data into an associative array. */ - public static function dataSubscriptionProvider(): Iterator + public function testToArray(): void { - yield ['{"Hello":"World"}', 'subscribe', ['platform' => 'npm', 'name' => 'utility', 'include_prerelease' => 'true']]; - yield ['{"Hello":"World"}', 'check', ['platform' => 'npm', 'name' => 'utility']]; - yield ['{"Hello":"World"}', 'update', ['platform' => 'npm', 'name' => 'utility', 'include_prerelease' => 'false']]; - yield ['{"Hello":"World"}', 'unsubscribe', ['platform' => 'npm', 'name' => 'utility']]; + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $response = $mockClient->user('dependencies', ['login' => 'ericsizemore']); + + self::assertInstanceOf(Response::class, $response); + self::assertSame(['Hello' => 'World'], Utils::toArray($response)); } /** - * Test the subscription endpoint. - * - * @param array $options + * Test the toObject function. It decodes the raw json data and creates a \stdClass object. */ - #[DataProvider('dataSubscriptionProvider')] - public function testSubscription(string $expected, string $endpoint, array $options): void + public function testToObject(): void { - $this->stub->client = $this->client; - $response = $this->stub->subscription($endpoint, $options); + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $response = $mockClient->user('dependencies', ['login' => 'ericsizemore']); self::assertInstanceOf(Response::class, $response); - self::assertSame($expected, $response->getBody()->getContents()); - } - /** - * Test the subscription endpoint with an invalid $endpoint arg specified. - */ - public function testSubscriptionInvalidEndpoint(): void - { - $this->stub->client = $this->client; - $this->expectException(InvalidArgumentException::class); - $this->stub->subscription('notvalid', ['platform' => 'npm', 'name' => 'utility']); + $expected = new stdClass(); + $expected->Hello = 'World'; + + self::assertEquals($expected, Utils::toObject($response)); } /** - * Test the subscription endpoint with a valid $endpoint arg and invalid $options specified. + * Test the user endpoint. + * + * @param array $options */ - public function testSubscriptionInvalidOptions(): void + #[DataProvider('dataUserProvider')] + public function testUser(string $expected, string $endpoint, array $options): void { - $this->stub->client = $this->client; - $this->expectException(InvalidArgumentException::class); - $this->stub->subscription('check', ['huh' => 'what']); + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $response = $mockClient->user($endpoint, $options); + + self::assertInstanceOf(Response::class, $response); + self::assertSame($expected, $response->getBody()->getContents()); } /** - * Test the toRaw function. It should return the raw response json. + * Test the user endpoint with an invalid $endpoint arg specified. */ - public function testRaw(): void + public function testUserInvalidEndpoint(): void { - $this->stub->client = $this->client; - $response = $this->stub->user('dependencies', ['login' => 'ericsizemore']); - - self::assertInstanceOf(Response::class, $response); - self::assertSame('{"Hello":"World"}', $this->stub->raw($response)); + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $this->expectException(InvalidEndpointException::class); + $mockClient->user('notvalid', ['login' => 'ericsizemore']); } /** - * Test the toArray function. It decodes the raw json data into an associative array. + * Test the user endpoint with a valid $endpoint arg and invalid $options specified. */ - public function testToArray(): void + public function testUserInvalidOptions(): void { - $this->stub->client = $this->client; - $response = $this->stub->user('dependencies', ['login' => 'ericsizemore']); - - self::assertInstanceOf(Response::class, $response); - self::assertSame(['Hello' => 'World'], $this->stub->toArray($response)); + $mockHandler = new MockHandler([$this->responses['valid']]); + $mockClient = $this->mockClient($this->testApiKey, $mockHandler); + $this->expectException(InvalidEndpointOptionsException::class); + $mockClient->user('packages', ['huh' => 'what']); } /** - * Test the toObject function. It decodes the raw json data and creates a \stdClass object. + * Creates a mock for testing. */ - public function testToObject(): void + private function mockClient(string $apiKey, ?MockHandler $mockHandler = null): LibrariesIO&MockObject { - $this->stub->client = $this->client; - $response = $this->stub->user('dependencies', ['login' => 'ericsizemore']); - - self::assertInstanceOf(Response::class, $response); + // Create a mock + //$handlerStack = HandlerStack::create($mockHandler); - $expected = new stdClass(); - $expected->Hello = 'World'; - - self::assertEquals($expected, $this->stub->toObject($response)); + return $this + ->getMockBuilder(LibrariesIO::class) + ->setConstructorArgs([$apiKey, sys_get_temp_dir(), ['_mockHandler' => $mockHandler]]) + ->onlyMethods([]) + ->getMock(); } }