diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 564da8b4..76f6f698 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -15,7 +15,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" - name: PHPStan uses: "docker://oskarstark/phpstan-ga" env: diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index dc803879..cb0e76bc 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -69,7 +69,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" with: fetch-depth: 2 @@ -79,6 +79,7 @@ jobs: php-version: "${{ matrix.php-version }}" coverage: "pcov" ini-values: "zend.assertions=1" + tools: "flex" - name: "Enforce using stable dependencies" run: "composer config minimum-stability stable" @@ -86,11 +87,10 @@ jobs: - name: "Add dependencies and enable flex" run: | - composer require --no-update symfony/flex ${{ matrix.dependencies }} - composer config --no-plugins allow-plugins.symfony/flex true + composer require --no-update ${{ matrix.dependencies }} - name: "Install dependencies with Composer" - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" with: dependency-versions: "${{ matrix.dependency-versions }}" composer-options: "${{ matrix.composer-options }}" diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index 289c83c3..67f68fe1 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -15,7 +15,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v3" + uses: "actions/checkout@v4" - name: "PHP-CS-Fixer" uses: "docker://oskarstark/php-cs-fixer-ga:3.26.0" with: diff --git a/.gitignore b/.gitignore index 3a9ff611..f1905e9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /.phpunit.result.cache -/.php_cs.cache +/.php-cs-fixer.cache /behat.yml /build/ /composer.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 08176801..3b3a7013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,26 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" betwee # Version 1 +# 1.34.3 - 2024-09-01 + +- Same as 1.34.2 but tagged on the 1.x banch instead of the feature branch. + +# 1.34.2 - 2024-09-01 + +- More cleanup of the rate-limiter configuration. The service name is the full service name of the rate limiter, e.g. `limiter.my_name` when configuring `framework.rate_limiter.my_name`. + +# 1.34.1 - 2024-09-01 + +- The rate-limiter name in the throttle plugin configuration is required. + +# 1.34.0 - 2024-06-17 + +- Support to configure the throttle plugin. + +# 1.33.1 - 2024-05-27 + +- Fixed extension to depend on the DependencyInjection component rather than the HttpKernel. + # 1.33.0 - 2024-02-27 - Support php-http/cache-plugin 2.0 and bump minimal version to 1.7 by defaulting the stream factory for cache to `httplug.psr17_stream_factory` (#448). diff --git a/composer.json b/composer.json index 3c5d0cb1..7d89d691 100644 --- a/composer.json +++ b/composer.json @@ -42,29 +42,30 @@ "symfony/options-resolver": "^6.4 || ^7.1" }, "conflict": { + "kriswallsmith/buzz": "<0.17", "php-http/guzzle6-adapter": "<1.1", "php-http/cache-plugin": "<1.7", "php-http/curl-client": "<2.0", "php-http/socket-client": "<2.0", - "kriswallsmith/buzz": "<0.17", - "php-http/react-adapter": "<3.0" + "php-http/react-adapter": "<3.0", + "php-http/throttle-plugin": "<1.1" }, "require-dev": { "guzzlehttp/psr7": "^1.7 || ^2.0", "matthiasnoback/symfony-config-test": "^5.2", - "matthiasnoback/symfony-dependency-injection-test": "^4.0 || ^5.0", + "matthiasnoback/symfony-dependency-injection-test": "^4.3.1 || ^5.0", "nyholm/nsa": "^1.1", "nyholm/psr7": "^1.2.1", "php-http/cache-plugin": "^1.7", "php-http/mock-client": "^1.2", "php-http/promise": "^1.0", + "php-http/throttle-plugin": "^1.1", "phpunit/phpunit": "^9", "symfony/browser-kit": "^6.4 || ^7.1", "symfony/cache": "^6.4 || ^7.1", "symfony/dom-crawler": "^6.4 || ^7.1", "symfony/framework-bundle": "^6.4 || ^7.1", "symfony/http-foundation": "^6.4 || ^7.1", - "symfony/phpunit-bridge": "^7.1", "symfony/stopwatch": "^6.4 || ^7.1", "symfony/twig-bundle": "^6.4 || ^7.1", "symfony/web-profiler-bundle": "^6.4 || ^7.1", @@ -97,7 +98,7 @@ }, "prefer-stable": false, "scripts": { - "test": "vendor/bin/simple-phpunit", - "test-ci": "vendor/bin/simple-phpunit --coverage-text --coverage-clover=build/coverage.xml" + "test": "vendor/bin/phpunit", + "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml" } } diff --git a/src/ClientFactory/AutoDiscoveryFactory.php b/src/ClientFactory/AutoDiscoveryFactory.php index bdda63a1..9578c2dc 100644 --- a/src/ClientFactory/AutoDiscoveryFactory.php +++ b/src/ClientFactory/AutoDiscoveryFactory.php @@ -10,6 +10,8 @@ * Use auto discovery to find a HTTP client. * * @author Tobias Nyholm + * + * @final */ class AutoDiscoveryFactory implements ClientFactory { diff --git a/src/ClientFactory/BuzzFactory.php b/src/ClientFactory/BuzzFactory.php index 904bcf0c..0e219c4f 100644 --- a/src/ClientFactory/BuzzFactory.php +++ b/src/ClientFactory/BuzzFactory.php @@ -10,6 +10,8 @@ /** * @author Tobias Nyholm + * + * @final */ class BuzzFactory implements ClientFactory { diff --git a/src/ClientFactory/CurlFactory.php b/src/ClientFactory/CurlFactory.php index c95fb455..db3d667e 100644 --- a/src/ClientFactory/CurlFactory.php +++ b/src/ClientFactory/CurlFactory.php @@ -10,12 +10,14 @@ /** * @author Tobias Nyholm + * + * @final */ class CurlFactory implements ClientFactory { public function __construct( private readonly ResponseFactoryInterface $responseFactory, - private readonly StreamFactoryInterface $streamFactory + private readonly StreamFactoryInterface $streamFactory, ) { } diff --git a/src/ClientFactory/Guzzle6Factory.php b/src/ClientFactory/Guzzle6Factory.php index 590eaaef..17ca3233 100644 --- a/src/ClientFactory/Guzzle6Factory.php +++ b/src/ClientFactory/Guzzle6Factory.php @@ -8,6 +8,8 @@ /** * @author Tobias Nyholm + * + * @final */ class Guzzle6Factory implements ClientFactory { diff --git a/src/ClientFactory/Guzzle7Factory.php b/src/ClientFactory/Guzzle7Factory.php index 431900ef..90143ab1 100644 --- a/src/ClientFactory/Guzzle7Factory.php +++ b/src/ClientFactory/Guzzle7Factory.php @@ -8,6 +8,8 @@ /** * @author Tobias Nyholm + * + * @final */ class Guzzle7Factory implements ClientFactory { diff --git a/src/ClientFactory/ReactFactory.php b/src/ClientFactory/ReactFactory.php index 61b48147..b1c66581 100644 --- a/src/ClientFactory/ReactFactory.php +++ b/src/ClientFactory/ReactFactory.php @@ -8,6 +8,8 @@ /** * @author Tobias Nyholm + * + * @final */ class ReactFactory implements ClientFactory { diff --git a/src/ClientFactory/SocketFactory.php b/src/ClientFactory/SocketFactory.php index c77e7d59..2424df3b 100644 --- a/src/ClientFactory/SocketFactory.php +++ b/src/ClientFactory/SocketFactory.php @@ -8,6 +8,8 @@ /** * @author Tobias Nyholm + * + * @final */ class SocketFactory implements ClientFactory { diff --git a/src/ClientFactory/SymfonyFactory.php b/src/ClientFactory/SymfonyFactory.php index 80809bf3..46485bf0 100644 --- a/src/ClientFactory/SymfonyFactory.php +++ b/src/ClientFactory/SymfonyFactory.php @@ -11,12 +11,14 @@ /** * @author Tobias Nyholm + * + * @final */ class SymfonyFactory implements ClientFactory { public function __construct( private readonly ResponseFactoryInterface $responseFactory, - private readonly StreamFactoryInterface $streamFactory + private readonly StreamFactoryInterface $streamFactory, ) { } diff --git a/src/Collector/Collector.php b/src/Collector/Collector.php index c02575f4..f53a0e2a 100644 --- a/src/Collector/Collector.php +++ b/src/Collector/Collector.php @@ -18,6 +18,8 @@ * @author Fabien Bourigault * * @internal + * + * @final */ class Collector extends DataCollector { diff --git a/src/Collector/Formatter.php b/src/Collector/Formatter.php index b9d0218d..701d9243 100644 --- a/src/Collector/Formatter.php +++ b/src/Collector/Formatter.php @@ -19,6 +19,8 @@ * @author Fabien Bourigault * * @internal + * + * @final */ class Formatter implements MessageFormatter { diff --git a/src/Collector/PluginClientFactory.php b/src/Collector/PluginClientFactory.php index fdf48a10..9402514d 100644 --- a/src/Collector/PluginClientFactory.php +++ b/src/Collector/PluginClientFactory.php @@ -23,7 +23,7 @@ final class PluginClientFactory public function __construct( private readonly Collector $collector, private readonly Formatter $formatter, - private readonly Stopwatch $stopwatch + private readonly Stopwatch $stopwatch, ) { } diff --git a/src/Collector/ProfileClient.php b/src/Collector/ProfileClient.php index 966f759b..5c350885 100644 --- a/src/Collector/ProfileClient.php +++ b/src/Collector/ProfileClient.php @@ -21,6 +21,8 @@ * @author Fabien Bourigault * * @internal + * + * @final */ class ProfileClient implements ClientInterface, HttpAsyncClient { @@ -40,7 +42,7 @@ public function __construct( $client, private readonly Collector $collector, private readonly Formatter $formatter, - private readonly Stopwatch $stopwatch + private readonly Stopwatch $stopwatch, ) { if (!($client instanceof ClientInterface && $client instanceof HttpAsyncClient)) { $client = new FlexibleHttpClient($client); diff --git a/src/Collector/ProfileClientFactory.php b/src/Collector/ProfileClientFactory.php index e84d80b6..4140a313 100644 --- a/src/Collector/ProfileClientFactory.php +++ b/src/Collector/ProfileClientFactory.php @@ -16,6 +16,8 @@ * @author Fabien Bourigault * * @internal + * + * @final */ class ProfileClientFactory implements ClientFactory { @@ -28,7 +30,7 @@ public function __construct( ClientFactory|callable $factory, private readonly Collector $collector, private readonly Formatter $formatter, - private readonly Stopwatch $stopwatch + private readonly Stopwatch $stopwatch, ) { $this->factory = $factory; } diff --git a/src/Collector/ProfilePlugin.php b/src/Collector/ProfilePlugin.php index 6d30eea9..54f7fd4e 100644 --- a/src/Collector/ProfilePlugin.php +++ b/src/Collector/ProfilePlugin.php @@ -15,6 +15,8 @@ * @author Fabien Bourigault * * @internal + * + * @final */ class ProfilePlugin implements Plugin { @@ -74,7 +76,7 @@ private function onException( RequestInterface $request, Profile $profile, \Exception $exception, - Stack $stack + Stack $stack, ): void { $profile->setFailed(true); $profile->setResponse($this->formatter->formatException($exception)); diff --git a/src/Collector/StackPlugin.php b/src/Collector/StackPlugin.php index 4f0ebdb7..c4353b36 100644 --- a/src/Collector/StackPlugin.php +++ b/src/Collector/StackPlugin.php @@ -15,6 +15,8 @@ * @author Fabien Bourigault * * @internal + * + * @final */ class StackPlugin implements Plugin { diff --git a/src/Collector/Twig/HttpMessageMarkupExtension.php b/src/Collector/Twig/HttpMessageMarkupExtension.php index c95fe212..a373abc3 100644 --- a/src/Collector/Twig/HttpMessageMarkupExtension.php +++ b/src/Collector/Twig/HttpMessageMarkupExtension.php @@ -13,6 +13,8 @@ /** * @author Tobias Nyholm + * + * @final */ class HttpMessageMarkupExtension extends AbstractExtension { diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 327a0e91..b6086ee2 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -31,6 +31,8 @@ * * @author David Buchmann * @author Tobias Nyholm + * + * @final */ class Configuration implements ConfigurationInterface { @@ -593,7 +595,7 @@ private function addSharedPluginNodes(ArrayNodeDefinition $pluginNode, bool $dis ->end(); // End stopwatch plugin - $error = $children->arrayNode('error') + $children->arrayNode('error') ->canBeEnabled() ->addDefaultsIfNotSet() ->children() @@ -601,6 +603,30 @@ private function addSharedPluginNodes(ArrayNodeDefinition $pluginNode, bool $dis ->end() ->end(); // End error plugin + + $children->arrayNode('throttle') + ->canBeEnabled() + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('name') + ->isRequired() + ->info('Rate limiter service name from symfony/rate-limiter configuration. E.g. for a rate limiter http_client you specify limiter.http_client here') + ->end() + ->scalarNode('key') + ->defaultNull() + ->info('Key to avoid sharing this rate limiter with other clients or other services. You can use the name of the client for example.') + ->end() + ->integerNode('tokens') + ->defaultValue(1) + ->info('How many tokens spending per request') + ->end() + ->floatNode('max_time') + ->defaultNull() + ->info('Maximum accepted waiting time in seconds') + ->end() + ->end() + ->end(); + // End throttle plugin } /** diff --git a/src/DependencyInjection/HttplugExtension.php b/src/DependencyInjection/HttplugExtension.php index d22d5e5a..13dd6f6d 100644 --- a/src/DependencyInjection/HttplugExtension.php +++ b/src/DependencyInjection/HttplugExtension.php @@ -10,6 +10,7 @@ use Http\Client\Common\HttpMethodsClient; use Http\Client\Common\HttpMethodsClientInterface; use Http\Client\Common\Plugin\AuthenticationPlugin; +use Http\Client\Common\Plugin\ThrottlePlugin; use Http\Client\Common\PluginClient; use Http\Client\Common\PluginClientFactory; use Http\Client\HttpAsyncClient; @@ -33,11 +34,14 @@ use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\RateLimiter\LimiterInterface; use Twig\Environment as TwigEnvironment; /** * @author David Buchmann * @author Tobias Nyholm + * + * @final */ class HttplugExtension extends Extension { @@ -279,6 +283,24 @@ private function configurePluginByName(string $name, Definition $definition, arr break; + case 'throttle': + if (!\class_exists(ThrottlePlugin::class)) { + throw new InvalidConfigurationException('You need to require the Throttle Plugin to be able to use it: "composer require php-http/throttle-plugin".'); + } + + $limiterServiceId = $serviceId.'.'.$config['name']; + $container + ->register($limiterServiceId, LimiterInterface::class) + ->setFactory([new Reference($config['name']), 'create']) + ->addArgument($config['key']) + ->setPublic(false); + + $definition->replaceArgument(0, new Reference($limiterServiceId)); + $definition->setArgument('$tokens', $config['tokens']); + $definition->setArgument('$maxTime', $config['max_time']); + + break; + /* client specific plugins */ case 'add_host': diff --git a/src/Discovery/ConfiguredClientsStrategy.php b/src/Discovery/ConfiguredClientsStrategy.php index b3222e3a..99166abd 100644 --- a/src/Discovery/ConfiguredClientsStrategy.php +++ b/src/Discovery/ConfiguredClientsStrategy.php @@ -14,6 +14,8 @@ * we can use the web debug toolbar for clients found with the discovery. * * @author Tobias Nyholm + * + * @final */ class ConfiguredClientsStrategy implements DiscoveryStrategy { diff --git a/src/Discovery/ConfiguredClientsStrategyListener.php b/src/Discovery/ConfiguredClientsStrategyListener.php index cca90968..85d9d02b 100644 --- a/src/Discovery/ConfiguredClientsStrategyListener.php +++ b/src/Discovery/ConfiguredClientsStrategyListener.php @@ -9,6 +9,8 @@ /** * @author Wouter de Jong + * + * @final */ class ConfiguredClientsStrategyListener implements EventSubscriberInterface { diff --git a/src/HttplugBundle.php b/src/HttplugBundle.php index d6aa836d..c9d09a53 100644 --- a/src/HttplugBundle.php +++ b/src/HttplugBundle.php @@ -9,6 +9,8 @@ /** * @author David Buchmann * @author Tobias Nyholm + * + * @final */ class HttplugBundle extends Bundle { diff --git a/src/Resources/config/plugins.xml b/src/Resources/config/plugins.xml index 749407c4..9f9c0ec1 100644 --- a/src/Resources/config/plugins.xml +++ b/src/Resources/config/plugins.xml @@ -28,6 +28,9 @@ + + + diff --git a/tests/Unit/DependencyInjection/ConfigurationTest.php b/tests/Unit/DependencyInjection/ConfigurationTest.php index 83b2e557..08ee1f50 100644 --- a/tests/Unit/DependencyInjection/ConfigurationTest.php +++ b/tests/Unit/DependencyInjection/ConfigurationTest.php @@ -89,6 +89,12 @@ class ConfigurationTest extends AbstractExtensionConfigurationTestCase 'enabled' => false, 'only_server_exception' => false, ], + 'throttle' => [ + 'enabled' => false, + 'key' => null, + 'tokens' => 1, + 'max_time' => null, + ], ], 'discovery' => [ 'client' => 'auto', @@ -293,6 +299,12 @@ public function testSupportsAllConfigFormats(): void 'enabled' => false, 'only_server_exception' => false, ], + 'throttle' => [ + 'enabled' => false, + 'key' => null, + 'tokens' => 1, + 'max_time' => null, + ], ], 'discovery' => [ 'client' => 'auto', diff --git a/tests/Unit/DependencyInjection/HttplugExtensionTest.php b/tests/Unit/DependencyInjection/HttplugExtensionTest.php index a4a799b9..df3fee11 100644 --- a/tests/Unit/DependencyInjection/HttplugExtensionTest.php +++ b/tests/Unit/DependencyInjection/HttplugExtensionTest.php @@ -80,7 +80,7 @@ public function testConfigLoadService(): void public function testClientPlugins(): void { - $this->load([ + $config = [ 'clients' => [ 'acme' => [ 'factory' => 'httplug.factory.curl', @@ -131,6 +131,11 @@ public function testClientPlugins(): void 'headers' => ['X-FOO'], ], ], + [ + 'query_defaults' => [ + 'parameters' => ['locale' => 'en'], + ], + ], [ 'request_seekable_body' => [ 'use_file_buffer' => true, @@ -140,8 +145,8 @@ public function testClientPlugins(): void 'response_seekable_body' => true, ], [ - 'query_defaults' => [ - 'parameters' => ['locale' => 'en'], + 'throttle' => [ + 'name' => 'limiter.test', ], ], [ @@ -166,7 +171,9 @@ public function testClientPlugins(): void ], ], ], - ]); + ]; + + $this->load($config); $plugins = [ 'httplug.client.acme.plugin.decoder', @@ -179,9 +186,10 @@ public function testClientPlugins(): void 'httplug.client.acme.plugin.header_defaults', 'httplug.client.acme.plugin.header_set', 'httplug.client.acme.plugin.header_remove', + 'httplug.client.acme.plugin.query_defaults', 'httplug.client.acme.plugin.request_seekable_body', 'httplug.client.acme.plugin.response_seekable_body', - 'httplug.client.acme.plugin.query_defaults', + 'httplug.client.acme.plugin.throttle', 'httplug.client.acme.authentication.my_basic', 'httplug.client.acme.plugin.cache', 'httplug.client.acme.plugin.error',