diff --git a/CHANGELOG.md b/CHANGELOG.md index 4491df1..ffe9ddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 1.7.0 - unreleased + +### Added + +* Added `blacklisted_paths` option, which takes an array of `strings` (regular expressions) and allows to define paths, that shall not be cached in any case. + ## 1.6.0 - 2019-01-23 ### Added diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index 7139b97..7e94bc5 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -400,6 +400,98 @@ function it_caches_private_responses_when_allowed( $this->handleRequest($request, $next, function () {}); } + function it_does_not_store_responses_of_requests_to_blacklisted_paths( + CacheItemPoolInterface $pool, + CacheItemInterface $item, + RequestInterface $request, + ResponseInterface $response, + StreamFactory $streamFactory, + StreamInterface $stream + ) { + $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ + 'default_ttl' => 60, + 'cache_lifetime' => 1000, + 'blacklisted_paths' => ['\/foo'] + ]]); + + $httpBody = 'body'; + $stream->__toString()->willReturn($httpBody); + $stream->isSeekable()->willReturn(true); + + $request->getMethod()->willReturn('GET'); + $request->getUri()->willReturn('/foo'); + $request->getBody()->shouldBeCalled(); + + $response->getStatusCode()->willReturn(200); + $response->getBody()->willReturn($stream); + $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); + + $pool->getItem('231392a16d98e1cf631845c79b7d45f40bab08f3')->shouldBeCalled()->willReturn($item); + $item->isHit()->willReturn(false); + + $item->set($this->getCacheItemMatcher([ + 'response' => $response->getWrappedObject(), + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0 + ]))->willReturn($item)->shouldNotBeCalled(); + $pool->save(Argument::any())->shouldNotBeCalled(); + + $next = function (RequestInterface $request) use ($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $this->handleRequest($request, $next, function () {}); + } + + function it_stores_responses_of_requests_not_in_blacklisted_paths( + CacheItemPoolInterface $pool, + CacheItemInterface $item, + RequestInterface $request, + ResponseInterface $response, + StreamFactory $streamFactory, + StreamInterface $stream + ) { + $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ + 'default_ttl' => 60, + 'cache_lifetime' => 1000, + 'blacklisted_paths' => ['\/foo'] + ]]); + + $httpBody = 'body'; + $stream->__toString()->willReturn($httpBody); + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + + $request->getMethod()->willReturn('GET'); + $request->getUri()->willReturn('/'); + $request->getBody()->shouldBeCalled(); + + $response->getStatusCode()->willReturn(200); + $response->getBody()->willReturn($stream); + $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); + $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); + $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); + + $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $item->isHit()->willReturn(false); + $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); + + $item->set($this->getCacheItemMatcher([ + 'response' => $response->getWrappedObject(), + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => [] + ]))->willReturn($item)->shouldBeCalled(); + $pool->save(Argument::any())->shouldBeCalled(); + + $next = function (RequestInterface $request) use ($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $this->handleRequest($request, $next, function () {}); + } function it_can_be_initialized_with_custom_cache_key_generator( CacheItemPoolInterface $pool, diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 5ca9b6d..805d361 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -60,6 +60,7 @@ final class CachePlugin implements Plugin * we have to store the cache for a longer time than the server originally says it is valid for. * We store a cache item for $cache_lifetime + max age of the response. * @var array $methods list of request methods which can be cached + * @var array $blacklisted_paths list of regex patterns of paths explicitly not to be cached * @var array $respect_response_cache_directives list of cache directives this plugin will respect while caching responses * @var CacheKeyGenerator $cache_key_generator an object to generate the cache key. Defaults to a new instance of SimpleGenerator * @var CacheListener[] $cache_listeners an array of objects to act on the response based on the results of the cache check. @@ -72,10 +73,7 @@ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamF $this->streamFactory = $streamFactory; if (isset($config['respect_cache_headers']) && isset($config['respect_response_cache_directives'])) { - throw new \InvalidArgumentException( - 'You can\'t provide config option "respect_cache_headers" and "respect_response_cache_directives". '. - 'Use "respect_response_cache_directives" instead.' - ); + throw new \InvalidArgumentException('You can\'t provide config option "respect_cache_headers" and "respect_response_cache_directives". Use "respect_response_cache_directives" instead.'); } $optionsResolver = new OptionsResolver(); @@ -183,7 +181,7 @@ protected function doHandleRequest(RequestInterface $request, callable $next, ca return $this->handleCacheListeners($request, $this->createResponseFromCacheItem($cacheItem), true, $cacheItem); } - if ($this->isCacheable($response)) { + if ($this->isCacheable($response) && $this->isCacheableRequest($request)) { $bodyStream = $response->getBody(); $body = $bodyStream->__toString(); if ($bodyStream->isSeekable()) { @@ -266,6 +264,24 @@ protected function isCacheable(ResponseInterface $response) return true; } + /** + * Verify that we can cache this request. + * + * @param RequestInterface $request + * + * @return bool + */ + private function isCacheableRequest(RequestInterface $request) + { + foreach ($this->config['blacklisted_paths'] as $not_to_cache_path) { + if (1 === preg_match('/'.$not_to_cache_path.'/', $request->getUri())) { + return false; + } + } + + return true; + } + /** * Get the value of a parameter in the cache control header. * @@ -353,6 +369,7 @@ private function configureOptions(OptionsResolver $resolver) 'respect_response_cache_directives' => ['no-cache', 'private', 'max-age', 'no-store'], 'cache_key_generator' => null, 'cache_listeners' => [], + 'blacklisted_paths' => [], ]); $resolver->setAllowedTypes('cache_lifetime', ['int', 'null']); @@ -360,6 +377,7 @@ private function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('respect_cache_headers', ['bool', 'null']); $resolver->setAllowedTypes('methods', 'array'); $resolver->setAllowedTypes('cache_key_generator', ['null', 'Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator']); + $resolver->setAllowedTypes('blacklisted_paths', 'array'); $resolver->setAllowedValues('hash_algo', hash_algos()); $resolver->setAllowedValues('methods', function ($value) { /* RFC7230 sections 3.1.1 and 3.2.6 except limited to uppercase characters. */