diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..801f208 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/EventListener/AddHeadersListener.php b/EventListener/AddHeadersListener.php deleted file mode 100644 index 516ce45..0000000 --- a/EventListener/AddHeadersListener.php +++ /dev/null @@ -1,93 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\HttpCache\EventListener; - -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\State\Util\RequestAttributesExtractor; -use Symfony\Component\HttpKernel\Event\ResponseEvent; - -/** - * Configures cache HTTP headers for the current response. - * - * @author Kévin Dunglas - * - * @deprecated use \Symfony\EventListener\AddHeadersListener.php instead - */ -final class AddHeadersListener -{ - use OperationRequestInitiatorTrait; - - public function __construct(private readonly bool $etag = false, private readonly ?int $maxAge = null, private readonly ?int $sharedMaxAge = null, private readonly ?array $vary = null, private readonly ?bool $public = null, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly ?int $staleWhileRevalidate = null, private readonly ?int $staleIfError = null) - { - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; - } - - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - if (!$request->isMethodCacheable()) { - return; - } - - $attributes = RequestAttributesExtractor::extractAttributes($request); - if (\count($attributes) < 1) { - return; - } - - $response = $event->getResponse(); - - if (!$response->getContent() || !$response->isSuccessful()) { - return; - } - - $operation = $this->initializeOperation($request); - if ('api_platform.symfony.main_controller' === $operation?->getController()) { - return; - } - $resourceCacheHeaders = $attributes['cache_headers'] ?? $operation?->getCacheHeaders() ?? []; - - if ($this->etag && !$response->getEtag()) { - $response->setEtag(md5((string) $response->getContent())); - } - - if (null !== ($maxAge = $resourceCacheHeaders['max_age'] ?? $this->maxAge) && !$response->headers->hasCacheControlDirective('max-age')) { - $response->setMaxAge($maxAge); - } - - $vary = $resourceCacheHeaders['vary'] ?? $this->vary; - if (null !== $vary) { - $response->setVary(array_diff($vary, $response->getVary()), false); - } - - // if the public-property is defined and not yet set; apply it to the response - $public = ($resourceCacheHeaders['public'] ?? $this->public); - if (null !== $public && !$response->headers->hasCacheControlDirective('public')) { - $public ? $response->setPublic() : $response->setPrivate(); - } - - // Cache-Control "s-maxage" is only relevant is resource is not marked as "private" - if (false !== $public && null !== ($sharedMaxAge = $resourceCacheHeaders['shared_max_age'] ?? $this->sharedMaxAge) && !$response->headers->hasCacheControlDirective('s-maxage')) { - $response->setSharedMaxAge($sharedMaxAge); - } - - if (null !== ($staleWhileRevalidate = $resourceCacheHeaders['stale_while_revalidate'] ?? $this->staleWhileRevalidate) && !$response->headers->hasCacheControlDirective('stale-while-revalidate')) { - $response->headers->addCacheControlDirective('stale-while-revalidate', (string) $staleWhileRevalidate); - } - - if (null !== ($staleIfError = $resourceCacheHeaders['stale_if_error'] ?? $this->staleIfError) && !$response->headers->hasCacheControlDirective('stale-if-error')) { - $response->headers->addCacheControlDirective('stale-if-error', (string) $staleIfError); - } - } -} diff --git a/EventListener/AddTagsListener.php b/EventListener/AddTagsListener.php deleted file mode 100644 index 74cd37e..0000000 --- a/EventListener/AddTagsListener.php +++ /dev/null @@ -1,95 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\HttpCache\EventListener; - -use ApiPlatform\HttpCache\PurgerInterface; -use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\State\UriVariablesResolverTrait; -use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\State\Util\RequestAttributesExtractor; -use Symfony\Component\HttpKernel\Event\ResponseEvent; - -/** - * Sets the list of resources' IRIs included in this response in the configured cache tag HTTP header and/or "xkey" HTTP headers. - * - * By default the "Cache-Tags" HTTP header is used because it is supported by CloudFlare. - * - * @see https://developers.cloudflare.com/cache/how-to/purge-cache#add-cache-tag-http-response-headers - * - * The "xkey" is used because it is supported by Varnish. - * @see https://docs.varnish-software.com/varnish-cache-plus/vmods/ykey/ - * - * @author Kévin Dunglas - * - * @deprecated use \Symfony\EventListener\AddTagsListener.php instead - */ -final class AddTagsListener -{ - use OperationRequestInitiatorTrait; - use UriVariablesResolverTrait; - - public function __construct(private readonly IriConverterInterface $iriConverter, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly ?PurgerInterface $purger = null) - { - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; - } - - /** - * Adds the configured HTTP cache tag and "xkey" headers. - */ - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - $operation = $this->initializeOperation($request); - if ('api_platform.symfony.main_controller' === $operation?->getController()) { - return; - } - $response = $event->getResponse(); - - if ( - !$request->isMethodCacheable() - || !$response->isCacheable() - || (!$attributes = RequestAttributesExtractor::extractAttributes($request)) - ) { - return; - } - - $resources = $request->attributes->get('_resources'); - if ($operation instanceof CollectionOperationInterface) { - // Allows to purge collections - $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $attributes['resource_class']); - $iri = $this->iriConverter->getIriFromResource($attributes['resource_class'], UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => $uriVariables]); - - $resources[$iri] = $iri; - } - - if (!$resources) { - return; - } - - if (!$this->purger) { - $response->headers->set('Cache-Tags', implode(',', $resources)); - - return; - } - - $headers = $this->purger->getResponseHeaders($resources); - - foreach ($headers as $key => $value) { - $response->headers->set($key, $value); - } - } -} diff --git a/PurgerInterface.php b/PurgerInterface.php index 6b55712..b47127b 100644 --- a/PurgerInterface.php +++ b/PurgerInterface.php @@ -31,6 +31,8 @@ public function purge(array $iris): void; * Get the response header containing purged tags. * * @param string[] $iris + * + * @return array */ public function getResponseHeaders(array $iris): array; } diff --git a/SouinPurger.php b/SouinPurger.php index 70b3727..4fc2ff7 100644 --- a/SouinPurger.php +++ b/SouinPurger.php @@ -29,8 +29,8 @@ class SouinPurger extends SurrogateKeysPurger /** * @param HttpClientInterface[] $clients */ - public function __construct(iterable $clients) + public function __construct(iterable $clients, int $maxHeaderLength = self::MAX_HEADER_SIZE_PER_BATCH) { - parent::__construct($clients, self::MAX_HEADER_SIZE_PER_BATCH, self::HEADER, self::SEPARATOR); + parent::__construct($clients, $maxHeaderLength, self::HEADER, self::SEPARATOR); } } diff --git a/State/AddHeadersProcessor.php b/State/AddHeadersProcessor.php index e1c008b..48c431a 100644 --- a/State/AddHeadersProcessor.php +++ b/State/AddHeadersProcessor.php @@ -29,8 +29,16 @@ final class AddHeadersProcessor implements ProcessorInterface /** * @param ProcessorInterface $decorated */ - public function __construct(private readonly ProcessorInterface $decorated, private readonly bool $etag = false, private readonly ?int $maxAge = null, private readonly ?int $sharedMaxAge = null, private readonly ?array $vary = null, private readonly ?bool $public = null, private readonly ?int $staleWhileRevalidate = null, private readonly ?int $staleIfError = null) - { + public function __construct( + private readonly ProcessorInterface $decorated, + private readonly bool $etag = false, + private readonly ?int $maxAge = null, + private readonly ?int $sharedMaxAge = null, + private readonly ?array $vary = null, + private readonly ?bool $public = null, + private readonly ?int $staleWhileRevalidate = null, + private readonly ?int $staleIfError = null, + ) { } public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed @@ -52,38 +60,31 @@ public function process(mixed $data, Operation $operation, array $uriVariables = $resourceCacheHeaders = $operation->getCacheHeaders() ?? []; - if ($this->etag && !$response->getEtag()) { - $response->setEtag(md5((string) $content)); - } + $public = ($resourceCacheHeaders['public'] ?? $this->public); - if (null !== ($maxAge = $resourceCacheHeaders['max_age'] ?? $this->maxAge) && !$response->headers->hasCacheControlDirective('max-age')) { - $response->setMaxAge($maxAge); - } + $options = [ + 'etag' => $this->etag && !$response->getEtag() ? hash('xxh3', (string) $content) : null, + 'max_age' => null !== ($maxAge = $resourceCacheHeaders['max_age'] ?? $this->maxAge) && !$response->headers->hasCacheControlDirective('max-age') ? $maxAge : null, + // Cache-Control "s-maxage" is only relevant is resource is not marked as "private" + 's_maxage' => false !== $public && null !== ($sharedMaxAge = $resourceCacheHeaders['shared_max_age'] ?? $this->sharedMaxAge) && !$response->headers->hasCacheControlDirective('s-maxage') ? $sharedMaxAge : null, + 'public' => null !== $public && !$response->headers->hasCacheControlDirective('public') ? $public : null, + 'stale_while_revalidate' => null !== ($staleWhileRevalidate = $resourceCacheHeaders['stale_while_revalidate'] ?? $this->staleWhileRevalidate) && !$response->headers->hasCacheControlDirective('stale-while-revalidate') ? $staleWhileRevalidate : null, + 'stale_if_error' => null !== ($staleIfError = $resourceCacheHeaders['stale_if_error'] ?? $this->staleIfError) && !$response->headers->hasCacheControlDirective('stale-if-error') ? $staleIfError : null, + 'must_revalidate' => null !== ($mustRevalidate = $resourceCacheHeaders['must_revalidate'] ?? null) && !$response->headers->hasCacheControlDirective('must-revalidate') ? $mustRevalidate : null, + 'proxy_revalidate' => null !== ($proxyRevalidate = $resourceCacheHeaders['proxy_revalidate'] ?? null) && !$response->headers->hasCacheControlDirective('proxy-revalidate') ? $proxyRevalidate : null, + 'no_cache' => null !== ($noCache = $resourceCacheHeaders['no_cache'] ?? null) && !$response->headers->hasCacheControlDirective('no-cache') ? $noCache : null, + 'no_store' => null !== ($noStore = $resourceCacheHeaders['no_store'] ?? null) && !$response->headers->hasCacheControlDirective('no-store') ? $noStore : null, + 'no_transform' => null !== ($noTransform = $resourceCacheHeaders['no_transform'] ?? null) && !$response->headers->hasCacheControlDirective('no-transform') ? $noTransform : null, + 'immutable' => null !== ($immutable = $resourceCacheHeaders['immutable'] ?? null) && !$response->headers->hasCacheControlDirective('immutable') ? $immutable : null, + ]; + + $response->setCache($options); $vary = $resourceCacheHeaders['vary'] ?? $this->vary; if (null !== $vary) { $response->setVary(array_diff($vary, $response->getVary()), false); } - // if the public-property is defined and not yet set; apply it to the response - $public = ($resourceCacheHeaders['public'] ?? $this->public); - if (null !== $public && !$response->headers->hasCacheControlDirective('public')) { - $public ? $response->setPublic() : $response->setPrivate(); - } - - // Cache-Control "s-maxage" is only relevant is resource is not marked as "private" - if (false !== $public && null !== ($sharedMaxAge = $resourceCacheHeaders['shared_max_age'] ?? $this->sharedMaxAge) && !$response->headers->hasCacheControlDirective('s-maxage')) { - $response->setSharedMaxAge($sharedMaxAge); - } - - if (null !== ($staleWhileRevalidate = $resourceCacheHeaders['stale_while_revalidate'] ?? $this->staleWhileRevalidate) && !$response->headers->hasCacheControlDirective('stale-while-revalidate')) { - $response->headers->addCacheControlDirective('stale-while-revalidate', (string) $staleWhileRevalidate); - } - - if (null !== ($staleIfError = $resourceCacheHeaders['stale_if_error'] ?? $this->staleIfError) && !$response->headers->hasCacheControlDirective('stale-if-error')) { - $response->headers->addCacheControlDirective('stale-if-error', (string) $staleIfError); - } - return $response; } } diff --git a/Tests/SouinPurgerTest.php b/Tests/SouinPurgerTest.php index 1a07617..7bba11d 100644 --- a/Tests/SouinPurgerTest.php +++ b/Tests/SouinPurgerTest.php @@ -14,15 +14,13 @@ namespace ApiPlatform\HttpCache\Tests; use ApiPlatform\HttpCache\SouinPurger; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\Promise\PromiseInterface; -use GuzzleHttp\Psr7\Response; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Contracts\HttpClient\ResponseStreamInterface; /** * @author Sylvain Combraque @@ -59,35 +57,24 @@ private function generateXResourcesTags(int $number, int $minimum = 0): array public function testMultiChunkedTags(): void { - /** @var HttpClientInterface $client */ - $client = new class implements ClientInterface { + $client = new class implements HttpClientInterface { public array $sentRegexes = []; - public function send(RequestInterface $request, array $options = []): ResponseInterface - { - throw new \LogicException('Not implemented'); - } - - public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface - { - throw new \LogicException('Not implemented'); - } - - public function request($method, $uri, array $options = []): ResponseInterface + public function request(string $method, string $url, array $options = []): ResponseInterface { $this->sentRegexes[] = $options['headers']['Surrogate-Key']; - return new Response(); + return new MockResponse(); } - public function requestAsync($method, $uri, array $options = []): PromiseInterface + public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface { throw new \LogicException('Not implemented'); } - public function getConfig($option = null): void + public function withOptions(array $options): static { - throw new \LogicException('Not implemented'); + return $this; } }; $purger = new SouinPurger([$client]); @@ -96,71 +83,49 @@ public function getConfig($option = null): void self::assertSame([ implode(', ', $this->generateXResourcesTags(146)), implode(', ', $this->generateXResourcesTags(200, 146)), - ], $client->sentRegexes); // @phpstan-ignore-line + ], $client->sentRegexes); } public function testPurgeWithMultipleClients(): void { - /** @var HttpClientInterface $client1 */ - $client1 = new class implements ClientInterface { - public $requests = []; + $client1 = new class implements HttpClientInterface { + public array $requests = []; - public function send(RequestInterface $request, array $options = []): ResponseInterface - { - throw new \LogicException('Not implemented'); - } - - public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface - { - throw new \LogicException('Not implemented'); - } - - public function request($method, $uri, array $options = []): ResponseInterface + public function request(string $method, string $url, array $options = []): ResponseInterface { $this->requests[] = [$method, 'http://dummy_host/dummy_api_path/souin_api', $options]; - return new Response(); + return new MockResponse(); } - public function requestAsync($method, $uri, array $options = []): PromiseInterface + public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface { throw new \LogicException('Not implemented'); } - public function getConfig($option = null): void + public function withOptions(array $options): static { - throw new \LogicException('Not implemented'); + return $this; } }; - /** @var HttpClientInterface $client2 */ - $client2 = new class implements ClientInterface { - public $requests = []; + $client2 = new class implements HttpClientInterface { + public array $requests = []; - public function send(RequestInterface $request, array $options = []): ResponseInterface - { - throw new \LogicException('Not implemented'); - } - - public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface - { - throw new \LogicException('Not implemented'); - } - - public function request($method, $uri, array $options = []): ResponseInterface + public function request(string $method, string $url, array $options = []): ResponseInterface { $this->requests[] = [$method, 'http://dummy_host/dummy_api_path/souin_api', $options]; - return new Response(); + return new MockResponse(); } - public function requestAsync($method, $uri, array $options = []): PromiseInterface + public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface { throw new \LogicException('Not implemented'); } - public function getConfig($option = null): void + public function withOptions(array $options): static { - throw new \LogicException('Not implemented'); + return $this; } }; @@ -170,12 +135,12 @@ public function getConfig($option = null): void Request::METHOD_PURGE, 'http://dummy_host/dummy_api_path/souin_api', ['headers' => ['Surrogate-Key' => '/foo']], - ], $client1->requests[0]); // @phpstan-ignore-line + ], $client1->requests[0]); self::assertSame([ Request::METHOD_PURGE, 'http://dummy_host/dummy_api_path/souin_api', ['headers' => ['Surrogate-Key' => '/foo']], - ], $client2->requests[0]); // @phpstan-ignore-line + ], $client2->requests[0]); } public function testGetResponseHeaders(): void diff --git a/Tests/State/AddHeadersProcessorTest.php b/Tests/State/AddHeadersProcessorTest.php index 384ac10..5472b5e 100644 --- a/Tests/State/AddHeadersProcessorTest.php +++ b/Tests/State/AddHeadersProcessorTest.php @@ -19,38 +19,54 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\ResponseHeaderBag; class AddHeadersProcessorTest extends TestCase { - public function testAddHeaders(): void + public function testAddHeadersFromGlobalConfiguration(): void { $operation = new Get(); - $response = $this->createMock(Response::class); - $response->expects($this->once())->method('setEtag'); - $response->method('getContent')->willReturn('{}'); - $response->method('isSuccessful')->willReturn(true); - $response->headers = $this->createMock(ResponseHeaderBag::class); - $response->headers->method('hasCacheControlDirective')->with($this->logicalOr( - $this->identicalTo('public'), - $this->identicalTo('s-maxage'), - $this->identicalTo('max-age'), - $this->identicalTo('stale-while-revalidate'), - $this->identicalTo('stale-if-error'), - ))->willReturn(false); - $response->headers->expects($this->exactly(2))->method('addCacheControlDirective')->with($this->logicalOr( - $this->identicalTo('stale-while-revalidate'), - $this->identicalTo('stale-if-error'), - ), '10'); - $response->expects($this->once())->method('setPublic'); - $response->expects($this->once())->method('setMaxAge'); - $response->expects($this->once())->method('setSharedMaxAge'); - $request = $this->createMock(Request::class); - $request->method('isMethodCacheable')->willReturn(true); + $response = new Response('content'); + $request = new Request(); $context = ['request' => $request]; $decorated = $this->createMock(ProcessorInterface::class); $decorated->method('process')->willReturn($response); $processor = new AddHeadersProcessor($decorated, etag: true, maxAge: 100, sharedMaxAge: 200, vary: ['Accept', 'Accept-Encoding'], public: true, staleWhileRevalidate: 10, staleIfError: 10); + $processor->process($response, $operation, [], $context); + + self::assertSame('max-age=100, public, s-maxage=200, stale-if-error=10, stale-while-revalidate=10', $response->headers->get('cache-control')); + self::assertSame('"55f2b31a6acfaa64"', $response->headers->get('etag')); + self::assertSame(['Accept', 'Accept-Encoding'], $response->headers->all('vary')); + } + + public function testAddHeadersFromOperationConfiguration(): void + { + $operation = new Get( + cacheHeaders: [ + 'public' => false, + 'max_age' => 250, + 'shared_max_age' => 500, + 'stale_while_revalidate' => 30, + 'stale_if_error' => 15, + 'vary' => ['Authorization', 'Accept-Language'], + 'must_revalidate' => true, + 'proxy_revalidate' => true, + 'no_cache' => true, + 'no_store' => true, + 'no_transform' => true, + 'immutable' => true, + ], + ); + $response = new Response('content'); + $request = new Request(); + $context = ['request' => $request]; + $decorated = $this->createMock(ProcessorInterface::class); + $decorated->method('process')->willReturn($response); + $processor = new AddHeadersProcessor($decorated); + + $processor->process($response, $operation, [], $context); + + self::assertSame('immutable, max-age=250, must-revalidate, no-store, no-transform, private, proxy-revalidate, stale-if-error=15, stale-while-revalidate=30', $response->headers->get('cache-control')); + self::assertSame(['Authorization', 'Accept-Language'], $response->headers->all('vary')); } } diff --git a/Tests/State/AddTagsProcessorTest.php b/Tests/State/AddTagsProcessorTest.php index 4413bda..210258e 100644 --- a/Tests/State/AddTagsProcessorTest.php +++ b/Tests/State/AddTagsProcessorTest.php @@ -51,7 +51,7 @@ public function testAddTags(): void public function testAddTagsCollection(): void { - $operation = new GetCollection(class: 'Foo', uriVariables: ['id' => new Link()]); + $operation = new GetCollection(class: \stdClass::class, uriVariables: ['id' => new Link()]); $response = $this->createMock(Response::class); $response->method('isCacheable')->willReturn(true); $response->headers = $this->createMock(ResponseHeaderBag::class); @@ -65,7 +65,7 @@ public function testAddTagsCollection(): void $decorated = $this->createMock(ProcessorInterface::class); $decorated->method('process')->willReturn($response); $iriConverter = $this->createMock(IriConverterInterface::class); - $iriConverter->expects($this->once())->method('getIriFromResource')->with('Foo', UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => ['id' => 1]])->willReturn('/foos/1/bars'); + $iriConverter->expects($this->once())->method('getIriFromResource')->with(\stdClass::class, UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => ['id' => 1]])->willReturn('/foos/1/bars'); $processor = new AddTagsProcessor($decorated, $iriConverter); $processor->process($response, $operation, [], $context); } diff --git a/Tests/VarnishPurgerTest.php b/Tests/VarnishPurgerTest.php index 39d4f56..4352067 100644 --- a/Tests/VarnishPurgerTest.php +++ b/Tests/VarnishPurgerTest.php @@ -14,15 +14,14 @@ namespace ApiPlatform\HttpCache\Tests; use ApiPlatform\HttpCache\VarnishPurger; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\Promise\PromiseInterface; -use GuzzleHttp\Psr7\Response; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Contracts\HttpClient\ResponseStreamInterface; /** * @author Kévin Dunglas @@ -59,56 +58,42 @@ public function testPurge(): void public function testEmptyTags(): void { - $clientProphecy1 = $this->prophesize(ClientInterface::class); + $clientProphecy1 = $this->prophesize(HttpClientInterface::class); $clientProphecy1->request()->shouldNotBeCalled(); - /** @var HttpClientInterface $client */ $client = $clientProphecy1->reveal(); $purger = new VarnishPurger([$client]); $purger->purge([]); } - /** - * @dataProvider provideChunkHeaderCases - */ + #[DataProvider('provideChunkHeaderCases')] public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLength, array $iris, array $regexesToSend): void { - /** @var HttpClientInterface $client */ - $client = new class implements ClientInterface { + $client = new class implements HttpClientInterface { public array $sentRegexes = []; - public function send(RequestInterface $request, array $options = []): ResponseInterface - { - throw new \LogicException('Not implemented'); - } - - public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface - { - throw new \LogicException('Not implemented'); - } - - public function request($method, $uri, array $options = []): ResponseInterface + public function request(string $method, string $url, array $options = []): ResponseInterface { $this->sentRegexes[] = $options['headers']['ApiPlatform-Ban-Regex']; - return new Response(); + return new MockResponse(); } - public function requestAsync($method, $uri, array $options = []): PromiseInterface + public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface { throw new \LogicException('Not implemented'); } - public function getConfig($option = null): void + public function withOptions(array $options): static { - throw new \LogicException('Not implemented'); + return $this; } }; $purger = new VarnishPurger([$client], $maxHeaderLength); $purger->purge($iris); - self::assertSame($regexesToSend, $client->sentRegexes); // @phpstan-ignore-line + self::assertSame($regexesToSend, $client->sentRegexes); } public static function provideChunkHeaderCases(): \Generator diff --git a/Tests/VarnishXKeyPurgerTest.php b/Tests/VarnishXKeyPurgerTest.php index df16530..2340f22 100644 --- a/Tests/VarnishXKeyPurgerTest.php +++ b/Tests/VarnishXKeyPurgerTest.php @@ -15,14 +15,15 @@ use ApiPlatform\HttpCache\VarnishXKeyPurger; use GuzzleHttp\ClientInterface; -use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Contracts\HttpClient\ResponseStreamInterface; /** * @author Kévin Dunglas @@ -98,47 +99,34 @@ public function testCustomGlue(): void $purger->purge(['/foo', '/bar', '/baz']); } - /** - * @dataProvider provideChunkHeaderCases - */ + #[DataProvider('provideChunkHeaderCases')] public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLength, array $iris, array $keysToSend): void { - /** @var HttpClientInterface $client */ - $client = new class implements ClientInterface { + $client = new class implements HttpClientInterface { public array $sentKeys = []; - public function send(RequestInterface $request, array $options = []): ResponseInterface - { - throw new \LogicException('Not implemented'); - } - - public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface - { - throw new \LogicException('Not implemented'); - } - - public function request($method, $uri, array $options = []): ResponseInterface + public function request(string $method, string $url, array $options = []): ResponseInterface { $this->sentKeys[] = $options['headers']['xkey']; - return new Response(); + return new MockResponse(); } - public function requestAsync($method, $uri, array $options = []): PromiseInterface + public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface { throw new \LogicException('Not implemented'); } - public function getConfig($option = null): void + public function withOptions(array $options): static { - throw new \LogicException('Not implemented'); + return $this; } }; $purger = new VarnishXKeyPurger([$client], $maxHeaderLength); $purger->purge($iris); - self::assertSame($keysToSend, $client->sentKeys); // @phpstan-ignore-line + self::assertSame($keysToSend, $client->sentKeys); } public static function provideChunkHeaderCases(): \Generator diff --git a/composer.json b/composer.json index e0369cb..f816b08 100644 --- a/composer.json +++ b/composer.json @@ -22,18 +22,18 @@ } ], "require": { - "php": ">=8.1", - "api-platform/metadata": "^3.4 || ^4.0", - "api-platform/state": "^3.4 || ^4.0", - "symfony/http-foundation": "^6.4 || ^7.1" + "php": ">=8.2", + "api-platform/metadata": "^4.3", + "api-platform/state": "^4.3", + "symfony/http-foundation": "^6.4.14 || ^7.0 || ^8.0" }, "require-dev": { - "guzzlehttp/guzzle": "^6.0 || ^7.0", - "symfony/dependency-injection": "^6.4 || ^7.0", - "phpspec/prophecy-phpunit": "^2.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", - "symfony/http-client": "^6.4 || ^7.0", - "sebastian/comparator": "<5.0" + "guzzlehttp/guzzle": "^6.0 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0", + "phpspec/prophecy-phpunit": "^2.2", + "symfony/http-client": "^6.4 || ^7.0 || ^8.0", + "symfony/type-info": "^7.3 || ^8.0", + "phpunit/phpunit": "^12.2" }, "autoload": { "psr-4": { @@ -55,11 +55,13 @@ }, "extra": { "branch-alias": { - "dev-main": "4.0.x-dev", - "dev-3.4": "3.4.x-dev" + "dev-main": "4.4.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0 || ^8.0" }, "thanks": { "name": "api-platform/api-platform", @@ -68,5 +70,7 @@ }, "scripts": { "test": "./vendor/bin/phpunit" - } + }, + "minimum-stability": "beta", + "prefer-stable": true } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d2a2213..f9a00ac 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,31 +1,23 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + trigger_deprecation + + + ./ + + + ./Tests + ./vendor + + -