diff --git a/doc/proxy-clients.rst b/doc/proxy-clients.rst index 298f57e1..e21e0b5a 100644 --- a/doc/proxy-clients.rst +++ b/doc/proxy-clients.rst @@ -19,18 +19,19 @@ Supported invalidation methods Not all clients support all :ref:`invalidation methods `. This table provides of methods supported by each proxy client: -============= ======= ======= ======= ======= ======= -Client Purge Refresh Ban Tagging Clear -============= ======= ======= ======= ======= ======= -Varnish ✓ ✓ ✓ ✓ -Fastly ✓ ✓ ✓ ✓ +============= ======= ======= ======= ======= ========= ======= +Client Purge Refresh Ban Tagging Prefix(*) Clear +============= ======= ======= ======= ======= ========= ======= +Varnish ✓ ✓ ✓ ✓ ✓ +Fastly ✓ ✓ ✓ ✓ NGINX ✓ ✓ -Symfony Cache ✓ ✓ ✓ (1) ✓ (1) -Cloudflare ✓ ✓ (2) ✓ -Noop ✓ ✓ ✓ ✓ ✓ -Multiplexer ✓ ✓ ✓ ✓ ✓ -============= ======= ======= ======= ======= ======= +Symfony Cache ✓ ✓ ✓ (1) ✓ (1) +Cloudflare ✓ ✓ (2) ✓ (2) ✓ +Noop ✓ ✓ ✓ ✓ ✓ ✓ +Multiplexer ✓ ✓ ✓ ✓ ✓ ✓ +============= ======= ======= ======= ======= ========= ======= +| (*): A limited version of Ban, that allows to invalidate by the beginning of a path | (1): Only when using `Toflar Psr6Store`_. | (2): Only available with `Cloudflare Enterprise`_. @@ -357,7 +358,7 @@ the HttpDispatcher is not available for Cloudflare):: Cloudflare supports different cache purge methods depending on your account. All Cloudflare accounts support purging the cache by URL and clearing all cache items. You need a `Cloudflare Enterprise`_ account to purge by cache - tags. + tags or prefixes. Zone identifier ^^^^^^^^^^^^^^^ diff --git a/src/CacheInvalidator.php b/src/CacheInvalidator.php index fe81a60d..86c38a8f 100644 --- a/src/CacheInvalidator.php +++ b/src/CacheInvalidator.php @@ -18,6 +18,7 @@ use FOS\HttpCache\Exception\UnsupportedProxyOperationException; use FOS\HttpCache\ProxyClient\Invalidation\BanCapable; use FOS\HttpCache\ProxyClient\Invalidation\ClearCapable; +use FOS\HttpCache\ProxyClient\Invalidation\PrefixCapable; use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable; use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable; use FOS\HttpCache\ProxyClient\Invalidation\TagCapable; @@ -216,6 +217,20 @@ public function invalidateTags(array $tags): static return $this; } + public function invalidatePrefixes(array $prefixes): static + { + if (!$this->cache instanceof PrefixCapable) { + throw UnsupportedProxyOperationException::cacheDoesNotImplement('Prefixes'); + } + if (!$prefixes) { + return $this; + } + + $this->cache->invalidatePrefixes($prefixes); + + return $this; + } + /** * Invalidate URLs based on a regular expression for the URI, an optional * content type and optional limit to certain hosts. diff --git a/src/ProxyClient/Cloudflare.php b/src/ProxyClient/Cloudflare.php index 785a7bf4..c9101568 100644 --- a/src/ProxyClient/Cloudflare.php +++ b/src/ProxyClient/Cloudflare.php @@ -12,6 +12,7 @@ namespace FOS\HttpCache\ProxyClient; use FOS\HttpCache\ProxyClient\Invalidation\ClearCapable; +use FOS\HttpCache\ProxyClient\Invalidation\PrefixCapable; use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable; use FOS\HttpCache\ProxyClient\Invalidation\TagCapable; use Psr\Http\Message\RequestFactoryInterface; @@ -27,7 +28,7 @@ * * @author Simon Jones */ -class Cloudflare extends HttpProxyClient implements ClearCapable, PurgeCapable, TagCapable +class Cloudflare extends HttpProxyClient implements ClearCapable, PrefixCapable, PurgeCapable, TagCapable { /** * @see https://api.cloudflare.com/#getting-started-endpoints @@ -87,6 +88,32 @@ public function invalidateTags(array $tags): static return $this; } + /** + * {@inheritdoc} + * + * URL prefix only available with Cloudflare enterprise plans. + * + * The prefixes need to include the domain name, but not the protocol, e.g. "www.example.com/path" + * + * @see https://developers.cloudflare.com/api/resources/cache/methods/purge/ + */ + public function invalidatePrefixes(array $prefixes): static + { + if (!$prefixes) { + return $this; + } + + $this->queueRequest( + 'POST', + sprintf(self::API_ENDPOINT.'/zones/%s/purge_cache', $this->options['zone_identifier']), + [], + false, + json_encode(['prefixes' => $prefixes], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES) + ); + + return $this; + } + /** * @see https://api.cloudflare.com/#zone-purge-files-by-url * @see https://developers.cloudflare.com/cache/how-to/purge-cache#purge-by-single-file-by-url For details on headers you can pass to clear the cache correctly diff --git a/src/ProxyClient/Invalidation/PrefixCapable.php b/src/ProxyClient/Invalidation/PrefixCapable.php new file mode 100644 index 00000000..2758726e --- /dev/null +++ b/src/ProxyClient/Invalidation/PrefixCapable.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\HttpCache\ProxyClient\Invalidation; + +use FOS\HttpCache\ProxyClient\ProxyClient; + +/** + * An HTTP cache that supports invalidation by a prefix, that is, removing + * or expiring objects from the cache starting with the given string or strings. + */ +interface PrefixCapable extends ProxyClient +{ + /** + * Remove/Expire cache objects based on URL prefixes. + * + * @param string[] $prefixes Prefixed objects that should be removed/expired from the cache. An empty prefix list should be ignored. + */ + public function invalidatePrefixes(array $prefixes): static; +} diff --git a/src/ProxyClient/MultiplexerClient.php b/src/ProxyClient/MultiplexerClient.php index a7caaedb..78faff0f 100644 --- a/src/ProxyClient/MultiplexerClient.php +++ b/src/ProxyClient/MultiplexerClient.php @@ -14,6 +14,7 @@ use FOS\HttpCache\Exception\InvalidArgumentException; use FOS\HttpCache\ProxyClient\Invalidation\BanCapable; use FOS\HttpCache\ProxyClient\Invalidation\ClearCapable; +use FOS\HttpCache\ProxyClient\Invalidation\PrefixCapable; use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable; use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable; use FOS\HttpCache\ProxyClient\Invalidation\TagCapable; @@ -23,7 +24,7 @@ * * @author Emanuele Panzeri */ -class MultiplexerClient implements BanCapable, PurgeCapable, RefreshCapable, TagCapable, ClearCapable +class MultiplexerClient implements BanCapable, PrefixCapable, PurgeCapable, RefreshCapable, TagCapable, ClearCapable { /** * @var ProxyClient[] @@ -93,6 +94,21 @@ public function invalidateTags(array $tags): static return $this; } + /** + * Forwards prefix invalidation request to all clients. + * + * {@inheritdoc} + */ + public function invalidatePrefixes(array $prefixes): static + { + if (!$prefixes) { + return $this; + } + $this->invoke(PrefixCapable::class, 'invalidatePrefixes', [$prefixes]); + + return $this; + } + /** * Forwards to all clients. * diff --git a/src/ProxyClient/Noop.php b/src/ProxyClient/Noop.php index 904b7c4b..93b09fa8 100644 --- a/src/ProxyClient/Noop.php +++ b/src/ProxyClient/Noop.php @@ -13,6 +13,7 @@ use FOS\HttpCache\ProxyClient\Invalidation\BanCapable; use FOS\HttpCache\ProxyClient\Invalidation\ClearCapable; +use FOS\HttpCache\ProxyClient\Invalidation\PrefixCapable; use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable; use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable; use FOS\HttpCache\ProxyClient\Invalidation\TagCapable; @@ -26,7 +27,7 @@ * * @author Gavin Staniforth */ -class Noop implements ProxyClient, BanCapable, PurgeCapable, RefreshCapable, TagCapable, ClearCapable +class Noop implements ProxyClient, BanCapable, PrefixCapable, PurgeCapable, RefreshCapable, TagCapable, ClearCapable { public function ban(array $headers): static { @@ -43,6 +44,11 @@ public function invalidateTags(array $tags): static return $this; } + public function invalidatePrefixes(array $prefixes): static + { + return $this; + } + public function purge(string $url, array $headers = []): static { return $this; diff --git a/src/ProxyClient/Varnish.php b/src/ProxyClient/Varnish.php index 6f7c6b02..68820ac4 100644 --- a/src/ProxyClient/Varnish.php +++ b/src/ProxyClient/Varnish.php @@ -13,6 +13,7 @@ use FOS\HttpCache\Exception\InvalidArgumentException; use FOS\HttpCache\ProxyClient\Invalidation\BanCapable; +use FOS\HttpCache\ProxyClient\Invalidation\PrefixCapable; use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable; use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable; use FOS\HttpCache\ProxyClient\Invalidation\TagCapable; @@ -36,7 +37,7 @@ * * @author David de Boer */ -class Varnish extends HttpProxyClient implements BanCapable, PurgeCapable, RefreshCapable, TagCapable +class Varnish extends HttpProxyClient implements BanCapable, PrefixCapable, PurgeCapable, RefreshCapable, TagCapable { public const HTTP_METHOD_BAN = 'BAN'; @@ -127,6 +128,22 @@ public function banPath(string $path, ?string $contentType = null, array|string| return $this->ban($headers); } + public function invalidatePrefixes(array $prefixes): static + { + if (!$prefixes) { + return $this; + } + + foreach ($prefixes as $prefix) { + $parts = explode('/', $prefix, 2); + $host = $parts[0]; + $path = isset($parts[1]) ? '/'.$parts[1] : '/'; + $this->banPath($path, null, $host); + } + + return $this; + } + public function purge(string $url, array $headers = []): static { $this->queueRequest(self::HTTP_METHOD_PURGE, $url, $headers); diff --git a/tests/Unit/ProxyClient/CloudflareTest.php b/tests/Unit/ProxyClient/CloudflareTest.php index a109c9bb..10c3dff3 100644 --- a/tests/Unit/ProxyClient/CloudflareTest.php +++ b/tests/Unit/ProxyClient/CloudflareTest.php @@ -71,6 +71,28 @@ function (RequestInterface $request) { $cloudflare->invalidateTags(['tag-one', 'tag-two']); } + public function testInvalidatePrefixesPurge(): void + { + $cloudflare = $this->getProxyClient(); + + $this->httpDispatcher->shouldReceive('invalidate')->once()->with( + \Mockery::on( + function (RequestInterface $request) { + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals('Bearer '.self::AUTH_TOKEN, current($request->getHeader('Authorization'))); + $this->assertEquals(sprintf('/client/v4/zones/%s/purge_cache', self::ZONE_IDENTIFIER), $request->getRequestTarget()); + + $this->assertEquals('{"prefixes":["example.com/one/","example.com/two/"]}', $request->getBody()->getContents()); + + return true; + } + ), + false + ); + + $cloudflare->invalidatePrefixes(['example.com/one/', 'example.com/two/']); + } + public function testPurge(): void { $cloudflare = $this->getProxyClient(); diff --git a/tests/Unit/ProxyClient/MultiplexerClientTest.php b/tests/Unit/ProxyClient/MultiplexerClientTest.php index c88e966d..362f6502 100644 --- a/tests/Unit/ProxyClient/MultiplexerClientTest.php +++ b/tests/Unit/ProxyClient/MultiplexerClientTest.php @@ -14,6 +14,7 @@ use FOS\HttpCache\Exception\InvalidArgumentException; use FOS\HttpCache\ProxyClient\Invalidation\BanCapable; use FOS\HttpCache\ProxyClient\Invalidation\ClearCapable; +use FOS\HttpCache\ProxyClient\Invalidation\PrefixCapable; use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable; use FOS\HttpCache\ProxyClient\Invalidation\RefreshCapable; use FOS\HttpCache\ProxyClient\Invalidation\TagCapable; @@ -103,6 +104,21 @@ public function testInvalidateTags(): void $this->assertSame($multiplexer, $multiplexer->invalidateTags($tags)); } + public function testInvalidatePrefixes(): void + { + $prefixes = ['example.com/one/', 'example.com/two/']; + + $mockClient = \Mockery::mock(PrefixCapable::class) + ->shouldReceive('invalidatePrefixes') + ->once() + ->with($prefixes) + ->getMock(); + + $multiplexer = new MultiplexerClient([$mockClient]); + + $this->assertSame($multiplexer, $multiplexer->invalidatePrefixes($prefixes)); + } + public function testRefresh(): void { $url = 'example.com'; diff --git a/tests/Unit/ProxyClient/NoopTest.php b/tests/Unit/ProxyClient/NoopTest.php index 878c2fdc..f29512b7 100644 --- a/tests/Unit/ProxyClient/NoopTest.php +++ b/tests/Unit/ProxyClient/NoopTest.php @@ -33,6 +33,11 @@ public function testInvalidateTags(): void $this->assertSame($this->noop, $this->noop->invalidateTags(['tag123'])); } + public function testInvalidatePrefixes(): void + { + $this->assertSame($this->noop, $this->noop->invalidatePrefixes(['example.com/one/'])); + } + public function testBanPath(): void { $this->assertSame($this->noop, $this->noop->banPath('/123')); diff --git a/tests/Unit/ProxyClient/VarnishTest.php b/tests/Unit/ProxyClient/VarnishTest.php index 7dde033d..f7894d81 100644 --- a/tests/Unit/ProxyClient/VarnishTest.php +++ b/tests/Unit/ProxyClient/VarnishTest.php @@ -236,4 +236,23 @@ function (RequestInterface $request) { $varnish->refresh('/fresh'); } + + public function testInvalidatePrefixes(): void + { + $varnish = new Varnish($this->httpDispatcher); + $this->httpDispatcher->shouldReceive('invalidate')->once()->with( + \Mockery::on( + function (RequestInterface $request) { + $this->assertEquals('BAN', $request->getMethod()); + $this->assertEquals('example.org', $request->getHeaderLine('X-Host')); + $this->assertEquals('/one/', $request->getHeaderLine('X-Url')); + $this->assertEquals('.*', $request->getHeaderLine('X-Content-Type')); + + return true; + } + ), + false + ); + $varnish->invalidatePrefixes(['example.org/one/']); + } }