From b98accf9e8710e49488502a54a6323bcf589f53b Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Wed, 27 Mar 2024 16:24:26 +0100 Subject: [PATCH] cleanup switch to psr --- CHANGELOG.md | 2 + composer.json | 10 ++-- doc/installation.rst | 24 ++-------- doc/proxy-clients.rst | 7 ++- src/ProxyClient/Cloudflare.php | 6 +-- src/ProxyClient/Fastly.php | 9 ++-- src/ProxyClient/HttpDispatcher.php | 26 +++++----- src/ProxyClient/HttpProxyClient.php | 37 ++++++++++----- src/Test/HttpClient.php | 25 +++++----- .../ProxyClient/HttpDispatcherTest.php | 6 +-- tests/Unit/ProxyClient/HttpDispatcherTest.php | 47 +++++++++---------- 11 files changed, 101 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca4d49e7f..b0d6ec119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ See also the [GitHub releases page](https://github.com/FriendsOfSymfony/FOSHttpC * Drop support for Symfony < 6.4 * Test with PHP 8.2 and 8.3 * Drop support for PHP < 8.1 +* Switched to PSR-17 message factories +* Switched some places to PSR-18 HTTP client. The main functionality needs the Httplug Async Client specification. There is no PSR for asynchronous clients. * Parameter and return type declarations where possible. * Ignore empty tag lists passed to `TagCapable::invalidateTags` so you don't need to check if there are tags or not. diff --git a/composer.json b/composer.json index 48457b124..e2ba72251 100644 --- a/composer.json +++ b/composer.json @@ -24,12 +24,11 @@ "php": "^8.1", "symfony/event-dispatcher": "^6.4 || ^7.0", "symfony/options-resolver": "^6.4 || ^7.0", - "php-http/client-implementation": "^1.0 || ^2.0", "php-http/client-common": "^1.1.0 || ^2.0", - "php-http/message": "^1.0 || ^2.0", "php-http/discovery": "^1.12", - "psr/http-factory": "^1.0", - "php-http/message-factory": "^1.0" + "php-http/async-client-implementation": "^1.1.0 || ^2.0", + "psr/http-client-implementation": "^1.0 || ^2.0", + "psr/http-factory": "^1.0" }, "require-dev": { "mockery/mockery": "^1.6.0", @@ -42,7 +41,8 @@ }, "conflict": { "toflar/psr6-symfony-http-cache-store": "<2.2.1", - "phpunit/phpunit": "<10" + "phpunit/phpunit": "<10", + "guzzlehttp/psr7": "<2" }, "suggest": { "friendsofsymfony/http-cache-bundle": "For integration with the Symfony framework", diff --git a/doc/installation.rst b/doc/installation.rst index 5b8e11fc5..d74b1bb19 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -9,28 +9,15 @@ and its dependencies using Composer_: .. code-block:: bash - $ composer require friendsofsymfony/http-cache - -The library relies on HTTPlug_ for sending invalidation requests over HTTP, so -you need to install an HTTPlug-compatible client or adapter first: - -.. code-block:: bash - - $ composer require php-http/guzzle6-adapter - -You also need a `PSR-7 message implementation`_. If you use Guzzle 6, Guzzle’s -implementation is already included. If you use another client, you need to -install one of the message implementations. Recommended: - -.. code-block:: bash - - $ composer require guzzlehttp/psr7 + $ composer require friendsofsymfony/http-cache -Alternatively: +The library relies on HTTPlug_ for asynchronously sending invalidation requests +over HTTP. There is no PSR for asynchronous HTTP client, you need to install an +HTTPlug-compatible client or adapter: .. code-block:: bash - $ composer require zendframework/zend-diactoros + $ composer require php-http/guzzle7-adapter Then install the FOSHttpCache library itself: @@ -73,6 +60,5 @@ invalidation requests: .. _Packagist: https://packagist.org/packages/friendsofsymfony/http-cache .. _Composer: http://getcomposer.org -.. _PSR-7 message implementation: https://packagist.org/providers/psr/http-message-implementation .. _Semantic Versioning: http://semver.org/ .. _HTTPlug: http://httplug.io diff --git a/doc/proxy-clients.rst b/doc/proxy-clients.rst index a602444f6..298f57e17 100644 --- a/doc/proxy-clients.rst +++ b/doc/proxy-clients.rst @@ -447,10 +447,9 @@ all requests together, reducing the performance impact of sending invalidation requests. .. _HTTPlug: http://httplug.io/ -.. _HTTPlug discovery: http://php-http.readthedocs.io/en/latest/discovery.html -.. _in the HTTPlug documentation: http://php-http.readthedocs.io/en/latest/clients.html -.. _HTTPlug plugins: http://php-http.readthedocs.io/en/latest/plugins/index.html -.. _message factory and URI factory: http://php-http.readthedocs.io/en/latest/message/message-factory.html +.. _HTTPlug discovery: https://docs.php-http.org/en/latest/discovery.html +.. _in the HTTPlug documentation: https://docs.php-http.org/en/latest/clients.html +.. _HTTPlug plugins: https://docs.php-http.org/en/latest/plugins/index.html .. _Toflar Psr6Store: https://github.com/Toflar/psr6-symfony-http-cache-store .. _Fastly Purge API: https://docs.fastly.com/api/purge .. _Cloudflare: https://developers.cloudflare.com/cache/ diff --git a/src/ProxyClient/Cloudflare.php b/src/ProxyClient/Cloudflare.php index 193b09f84..21445ed64 100644 --- a/src/ProxyClient/Cloudflare.php +++ b/src/ProxyClient/Cloudflare.php @@ -14,9 +14,9 @@ use FOS\HttpCache\ProxyClient\Invalidation\ClearCapable; use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable; use FOS\HttpCache\ProxyClient\Invalidation\TagCapable; +use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\UriInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Psr\Http\Message\RequestFactoryInterface; /** * Cloudflare HTTP cache invalidator. @@ -54,13 +54,13 @@ class Cloudflare extends HttpProxyClient implements ClearCapable, PurgeCapable, public function __construct( Dispatcher $dispatcher, array $options = [], - ?RequestFactoryInterface $messageFactory = null + ?RequestFactoryInterface $requestFactory = null ) { if (!function_exists('json_encode')) { throw new \Exception('ext-json is required for cloudflare invalidation'); } - parent::__construct($dispatcher, $options, $messageFactory); + parent::__construct($dispatcher, $options, $requestFactory); } /** diff --git a/src/ProxyClient/Fastly.php b/src/ProxyClient/Fastly.php index 53ab5458a..1d2bf8111 100644 --- a/src/ProxyClient/Fastly.php +++ b/src/ProxyClient/Fastly.php @@ -15,10 +15,9 @@ use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable; use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable; use FOS\HttpCache\ProxyClient\Invalidation\TagCapable; -use Http\Message\RequestFactory; +use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\UriInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Psr\Http\Message\RequestFactoryInterface; /** * Fastly HTTP cache invalidator. @@ -51,13 +50,13 @@ class Fastly extends HttpProxyClient implements ClearCapable, PurgeCapable, Refr public function __construct( Dispatcher $dispatcher, array $options = [], - ?RequestFactoryInterface $messageFactory = null + ?RequestFactoryInterface $requestFactory = null ) { if (!function_exists('json_encode')) { throw new \Exception('ext-json is required for fastly invalidation'); } - parent::__construct($dispatcher, $options, $messageFactory); + parent::__construct($dispatcher, $options, $requestFactory); } /** @@ -82,7 +81,7 @@ public function invalidateTags(array $tags): static $url, $headers, false, - json_encode(['surrogate_keys' => $tagChunk]) + json_encode(['surrogate_keys' => $tagChunk], JSON_THROW_ON_ERROR) ); } diff --git a/src/ProxyClient/HttpDispatcher.php b/src/ProxyClient/HttpDispatcher.php index ae38914de..9a19c11d3 100644 --- a/src/ProxyClient/HttpDispatcher.php +++ b/src/ProxyClient/HttpDispatcher.php @@ -66,18 +66,18 @@ class HttpDispatcher implements Dispatcher * class and overwrite getServers. Be sure to have some caching in * getServers. * - * @param string[] $servers Caching proxy server hostnames or IP - * addresses, including port if not port 80. - * E.g. ['127.0.0.1:6081'] - * @param string $baseUri Default application hostname, optionally - * including base URL, for purge and refresh - * requests (optional). This is required if - * you purge and refresh paths instead of - * absolute URLs - * @param HttpAsyncClient|null $httpClient Client capable of sending HTTP requests. If no - * client is supplied, a default one is created - * @param UriFactoryInterface|null $uriFactory Factory for PSR-7 URIs. If not specified, a - * default one is created + * @param string[] $servers Caching proxy server hostnames or IP + * addresses, including port if not port 80. + * E.g. ['127.0.0.1:6081'] + * @param string $baseUri Default application hostname, optionally + * including base URL, for purge and refresh + * requests (optional). This is required if + * you purge and refresh paths instead of + * absolute URLs + * @param HttpAsyncClient|null $httpClient Client capable of sending HTTP requests. If no + * client is supplied, a default one is created + * @param UriFactoryInterface|null $uriFactory Factory for PSR-7 URIs. If not specified, a + * default one is created */ public function __construct( array $servers, @@ -286,7 +286,7 @@ private function filterUri(string $uriString, array $allowedParts = []): UriInte try { $uri = $this->uriFactory->createUri($uriString); - } catch (\InvalidArgumentException $e) { + } catch (\InvalidArgumentException) { throw InvalidUrlException::invalidUrl($uriString); } diff --git a/src/ProxyClient/HttpProxyClient.php b/src/ProxyClient/HttpProxyClient.php index e8e9abc63..7115fd166 100644 --- a/src/ProxyClient/HttpProxyClient.php +++ b/src/ProxyClient/HttpProxyClient.php @@ -11,9 +11,9 @@ namespace FOS\HttpCache\ProxyClient; -use Http\Discovery\MessageFactoryDiscovery; use Http\Discovery\Psr17FactoryDiscovery; use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -31,6 +31,7 @@ abstract class HttpProxyClient implements ProxyClient private Dispatcher $httpDispatcher; private RequestFactoryInterface $requestFactory; + private StreamFactoryInterface $streamFactory; /** * The options configured in the constructor argument or default values. @@ -42,19 +43,23 @@ abstract class HttpProxyClient implements ProxyClient /** * The base class has no options. * - * @param Dispatcher $dispatcher Helper to send instructions to the caching proxy - * @param array $options Options for this client - * @param RequestFactoryInterface|null $messageFactory Factory for PSR-7 messages. If none supplied, - * a default one is created + * @param Dispatcher $dispatcher Helper to send instructions to the caching proxy + * @param array $options Options for this client + * @param RequestFactoryInterface|null $requestFactory Factory for PSR-7 messages. If none supplied, + * a default one is created + * @param StreamFactoryInterface|null $streamFactory Factory for PSR-7 streams. If none supplied, + * a default one is created */ public function __construct( Dispatcher $dispatcher, array $options = [], - ?RequestFactoryInterface $messageFactory = null + ?RequestFactoryInterface $requestFactory = null, + ?StreamFactoryInterface $streamFactory = null, ) { $this->httpDispatcher = $dispatcher; $this->options = $this->configureOptions()->resolve($options); - $this->requestFactory = $messageFactory ?: Psr17FactoryDiscovery::findRequestFactory(); + $this->requestFactory = $requestFactory ?: Psr17FactoryDiscovery::findRequestFactory(); + $this->streamFactory = $streamFactory ?: Psr17FactoryDiscovery::findStreamFactory(); } public function flush(): int @@ -79,10 +84,20 @@ protected function configureOptions(): OptionsResolver */ protected function queueRequest(string $method, UriInterface|string $url, array $headers, bool $validateHost = true, $body = null): void { - $this->httpDispatcher->invalidate( - $this->requestFactory->createRequest($method, $url, $headers, $body), - $validateHost - ); + $request = $this->requestFactory->createRequest($method, $url); + foreach ($headers as $name => $value) { + $request = $request->withHeader($name, $value); + } + if ($body) { + if (is_string($body)) { + $body = $this->streamFactory->createStream($body); + } elseif (is_resource($body)) { + $body = $this->streamFactory->createStreamFromResource($body); + } + $request = $request->withBody($body); + } + + $this->httpDispatcher->invalidate($request, $validateHost); } /** diff --git a/src/Test/HttpClient.php b/src/Test/HttpClient.php index 7a280c374..b2bb2c16c 100644 --- a/src/Test/HttpClient.php +++ b/src/Test/HttpClient.php @@ -11,10 +11,9 @@ namespace FOS\HttpCache\Test; -use Http\Client\HttpClient as PhpHttpClient; -use Http\Discovery\HttpClientDiscovery; -use Http\Discovery\MessageFactoryDiscovery; -use Http\Discovery\UriFactoryDiscovery; +use Http\Discovery\Psr17FactoryDiscovery; +use Http\Discovery\Psr18ClientDiscovery; +use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; @@ -27,7 +26,7 @@ class HttpClient /** * HTTP client for requests to the application. */ - private PhpHttpClient $httpClient; + private ClientInterface $httpClient; private string $hostname; @@ -72,10 +71,10 @@ public function sendRequest(RequestInterface $request): ResponseInterface /** * Get HTTP client for your application. */ - private function getHttpClient(): PhpHttpClient + private function getHttpClient(): ClientInterface { if (!isset($this->httpClient)) { - $this->httpClient = HttpClientDiscovery::find(); + $this->httpClient = Psr18ClientDiscovery::find(); } return $this->httpClient; @@ -102,11 +101,15 @@ private function createRequest(string $method, string $uri, array $headers): Req $uri = $uri->withScheme('http'); } - return MessageFactoryDiscovery::find()->createRequest( + $request = Psr17FactoryDiscovery::findRequestFactory()->createRequest( $method, - $uri, - $headers + $uri ); + foreach ($headers as $name => $value) { + $request = $request->withHeader($name, $value); + } + + return $request; } /** @@ -114,6 +117,6 @@ private function createRequest(string $method, string $uri, array $headers): Req */ private function createUri(string $uriString): UriInterface { - return UriFactoryDiscovery::find()->createUri($uriString); + return Psr17FactoryDiscovery::findUriFactory()->createUri($uriString); } } diff --git a/tests/Functional/ProxyClient/HttpDispatcherTest.php b/tests/Functional/ProxyClient/HttpDispatcherTest.php index c416c085c..ac3801b1f 100644 --- a/tests/Functional/ProxyClient/HttpDispatcherTest.php +++ b/tests/Functional/ProxyClient/HttpDispatcherTest.php @@ -15,14 +15,14 @@ use FOS\HttpCache\Exception\ProxyResponseException; use FOS\HttpCache\Exception\ProxyUnreachableException; use FOS\HttpCache\ProxyClient\HttpDispatcher; -use Http\Discovery\MessageFactoryDiscovery; +use Http\Discovery\Psr17FactoryDiscovery; use PHPUnit\Framework\TestCase; class HttpDispatcherTest extends TestCase { public function testNetworkError(): void { - $requestFactory = MessageFactoryDiscovery::find(); + $requestFactory = Psr17FactoryDiscovery::findRequestFactory(); $dispatcher = new HttpDispatcher(['localhost:1']); $dispatcher->invalidate($requestFactory->createRequest('GET', 'http://fos.test/foobar')); @@ -36,7 +36,7 @@ public function testNetworkError(): void public function testClientError(): void { - $requestFactory = MessageFactoryDiscovery::find(); + $requestFactory = Psr17FactoryDiscovery::findRequestFactory(); $dispatcher = new HttpDispatcher(['http://foshttpcache.readthedocs.io']); $dispatcher->invalidate($requestFactory->createRequest('GET', 'http://fos.test/this-url-should-not-exist')); diff --git a/tests/Unit/ProxyClient/HttpDispatcherTest.php b/tests/Unit/ProxyClient/HttpDispatcherTest.php index ce44ebcd1..840d079d4 100644 --- a/tests/Unit/ProxyClient/HttpDispatcherTest.php +++ b/tests/Unit/ProxyClient/HttpDispatcherTest.php @@ -21,16 +21,15 @@ use Http\Client\Exception\HttpException; use Http\Client\Exception\NetworkException; use Http\Client\HttpAsyncClient; -use Http\Discovery\MessageFactoryDiscovery; -use Http\Discovery\UriFactoryDiscovery; -use Http\Message\MessageFactory; -use Http\Message\UriFactory; +use Http\Discovery\Psr17FactoryDiscovery; use Http\Mock\Client; use Http\Promise\Promise; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\UriFactoryInterface; class HttpDispatcherTest extends TestCase { @@ -38,15 +37,15 @@ class HttpDispatcherTest extends TestCase private Client $httpClient; - private MessageFactory $messageFactory; + private RequestFactoryInterface $requestFactory; - private UriFactory $uriFactory; + private UriFactoryInterface $uriFactory; protected function setUp(): void { $this->httpClient = new Client(); - $this->messageFactory = MessageFactoryDiscovery::find(); - $this->uriFactory = UriFactoryDiscovery::find(); + $this->requestFactory = Psr17FactoryDiscovery::findRequestFactory(); + $this->uriFactory = Psr17FactoryDiscovery::findUriFactory(); } /** @@ -79,7 +78,7 @@ private function doTestException(\Exception $exception, string $type, string $me 'my_hostname.dev', $this->httpClient ); - $httpDispatcher->invalidate($this->messageFactory->createRequest('PURGE', '/path')); + $httpDispatcher->invalidate($this->requestFactory->createRequest('PURGE', '/path')); try { $httpDispatcher->flush(); @@ -93,7 +92,7 @@ private function doTestException(\Exception $exception, string $type, string $me } // Queue must now be empty, so exception above must not be thrown again. - $httpDispatcher->invalidate($this->messageFactory->createRequest('GET', '/path')); + $httpDispatcher->invalidate($this->requestFactory->createRequest('GET', '/path')); $httpDispatcher->flush(); } @@ -142,7 +141,7 @@ public function testMissingHostExceptionIsThrown(): void $this->httpClient ); - $request = $this->messageFactory->createRequest('PURGE', '/path/without/hostname'); + $request = $this->requestFactory->createRequest('PURGE', '/path/without/hostname'); $httpDispatcher->invalidate($request); } @@ -154,7 +153,7 @@ public function testBanWithoutBaseUri(): void $this->httpClient ); - $request = $this->messageFactory->createRequest('BAN', '/', ['X-Url' => '/foo/.*']); + $request = $this->requestFactory->createRequest('BAN', '/')->withHeader('X-Url', '/foo/.*'); $httpDispatcher->invalidate($request, false); $httpDispatcher->flush(); @@ -170,7 +169,7 @@ public function testSetBasePathWithHost(): void $this->httpClient ); - $request = $this->messageFactory->createRequest('PURGE', '/path'); + $request = $this->requestFactory->createRequest('PURGE', '/path'); $httpDispatcher->invalidate($request); $httpDispatcher->flush(); @@ -186,7 +185,7 @@ public function testServerWithUserInfo(): void $this->httpClient ); - $request = $this->messageFactory->createRequest('PURGE', '/path'); + $request = $this->requestFactory->createRequest('PURGE', '/path'); $httpDispatcher->invalidate($request); $httpDispatcher->flush(); @@ -205,7 +204,7 @@ public function testSetBasePathWithPath(): void 'http://fos.lo/my/path', $this->httpClient ); - $request = $this->messageFactory->createRequest('PURGE', '/append'); + $request = $this->requestFactory->createRequest('PURGE', '/append'); $httpDispatcher->invalidate($request); $httpDispatcher->flush(); @@ -217,7 +216,7 @@ public function testSetBasePathWithPath(): void public function testSetServersDefaultSchemeIsAdded(): void { $httpDispatcher = new HttpDispatcher(['127.0.0.1'], 'fos.lo', $this->httpClient); - $request = $this->messageFactory->createRequest('PURGE', '/some/path'); + $request = $this->requestFactory->createRequest('PURGE', '/some/path'); $httpDispatcher->invalidate($request); $httpDispatcher->flush(); @@ -229,7 +228,7 @@ public function testSchemeIsAdded(): void { $httpDispatcher = new HttpDispatcher(['127.0.0.1'], 'fos.lo', $this->httpClient); $uri = $this->uriFactory->createUri('/some/path')->withHost('goo.bar'); - $request = $this->messageFactory->createRequest('PURGE', $uri); + $request = $this->requestFactory->createRequest('PURGE', $uri); $httpDispatcher->invalidate($request); $httpDispatcher->flush(); @@ -240,7 +239,7 @@ public function testSchemeIsAdded(): void public function testPortIsAdded(): void { $httpDispatcher = new HttpDispatcher(['127.0.0.1:8080'], 'fos.lo', $this->httpClient); - $request = $this->messageFactory->createRequest('PURGE', '/some/path'); + $request = $this->requestFactory->createRequest('PURGE', '/some/path'); $httpDispatcher->invalidate($request); $httpDispatcher->flush(); @@ -315,8 +314,8 @@ function (RequestInterface $request) { 'fos.lo', $httpClient ); - $httpDispatcher->invalidate($this->messageFactory->createRequest('PURGE', '/a')); - $httpDispatcher->invalidate($this->messageFactory->createRequest('PURGE', '/b')); + $httpDispatcher->invalidate($this->requestFactory->createRequest('PURGE', '/a')); + $httpDispatcher->invalidate($this->requestFactory->createRequest('PURGE', '/b')); $this->assertEquals( 2, @@ -348,15 +347,15 @@ function (RequestInterface $request) { $httpDispatcher = new HttpDispatcher(['127.0.0.1', '127.0.0.2'], 'fos.lo', $httpClient); $httpDispatcher->invalidate( - $this->messageFactory->createRequest('PURGE', '/c', ['a' => 'b', 'c' => 'd']) + $this->requestFactory->createRequest('PURGE', '/c')->withHeader('a', 'b')->withHeader('c', 'd') ); $httpDispatcher->invalidate( // same request (header order is not significant) - $this->messageFactory->createRequest('PURGE', '/c', ['c' => 'd', 'a' => 'b']) + $this->requestFactory->createRequest('PURGE', '/c')->withHeader('c', 'd')->withHeader('a', 'b') ); // different request as headers different - $httpDispatcher->invalidate($this->messageFactory->createRequest('PURGE', '/c')); - $httpDispatcher->invalidate($this->messageFactory->createRequest('PURGE', '/c')); + $httpDispatcher->invalidate($this->requestFactory->createRequest('PURGE', '/c')); + $httpDispatcher->invalidate($this->requestFactory->createRequest('PURGE', '/c')); $this->assertEquals( 2,