diff --git a/spec/Cache/Generator/SimpleGeneratorSpec.php b/spec/Cache/Generator/SimpleGeneratorSpec.php new file mode 100644 index 0000000..9ee102c --- /dev/null +++ b/spec/Cache/Generator/SimpleGeneratorSpec.php @@ -0,0 +1,38 @@ +shouldHaveType('Http\Client\Common\Plugin\Cache\Generator\SimpleGenerator'); + } + + public function it_is_a_key_generator() + { + $this->shouldImplement('Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator'); + } + + public function it_generates_cache_from_request(RequestInterface $request) + { + $request->getMethod()->shouldBeCalled()->willReturn('GET'); + $request->getUri()->shouldBeCalled()->willReturn('http://example.com/foo'); + $request->getBody()->shouldBeCalled()->willReturn('bar'); + + $this->generate($request)->shouldReturn('GET http://example.com/foo bar'); + } + + public function it_generates_cache_from_request_with_no_body(RequestInterface $request) + { + $request->getMethod()->shouldBeCalled()->willReturn('GET'); + $request->getUri()->shouldBeCalled()->willReturn('http://example.com/foo'); + $request->getBody()->shouldBeCalled()->willReturn(''); + + // No extra space after uri + $this->generate($request)->shouldReturn('GET http://example.com/foo'); + } +} diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index b8f6560..af8a483 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -2,6 +2,7 @@ namespace spec\Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\Cache\Generator\SimpleGenerator; use Prophecy\Argument; use Http\Message\StreamFactory; use Http\Promise\FulfilledPromise; @@ -400,6 +401,47 @@ function it_caches_private_responses_when_allowed( } + function it_can_be_initialized_with_custom_cache_key_generator( + CacheItemPoolInterface $pool, + CacheItemInterface $item, + StreamFactory $streamFactory, + RequestInterface $request, + ResponseInterface $response, + StreamInterface $stream, + SimpleGenerator $generator + ) { + $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ + 'cache_key_generator' => $generator, + ]]); + + $generator->generate($request)->shouldBeCalled()->willReturn('foo'); + + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + $streamFactory->createStream(Argument::any())->willReturn($stream); + + $request->getMethod()->willReturn('GET'); + $request->getUri()->willReturn('/'); + $response->withBody(Argument::any())->willReturn($response); + + $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); + $item->isHit()->willReturn(true); + $item->get()->willReturn([ + 'response' => $response->getWrappedObject(), + 'body' => 'body', + 'expiresAt' => null, + 'createdAt' => 0, + 'etag' => [] + ]); + + $next = function (RequestInterface $request) use ($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $this->handleRequest($request, $next, function () {}); + } + + /** * Private function to match cache item data. * diff --git a/src/Cache/Generator/CacheKeyGenerator.php b/src/Cache/Generator/CacheKeyGenerator.php new file mode 100644 index 0000000..d351e57 --- /dev/null +++ b/src/Cache/Generator/CacheKeyGenerator.php @@ -0,0 +1,22 @@ + + */ +interface CacheKeyGenerator +{ + /** + * Generate a cache key from a Request. + * + * @param RequestInterface $request + * + * @return string + */ + public function generate(RequestInterface $request); +} diff --git a/src/Cache/Generator/SimpleGenerator.php b/src/Cache/Generator/SimpleGenerator.php new file mode 100644 index 0000000..4f0ee90 --- /dev/null +++ b/src/Cache/Generator/SimpleGenerator.php @@ -0,0 +1,23 @@ + + */ +class SimpleGenerator implements CacheKeyGenerator +{ + public function generate(RequestInterface $request) + { + $body = (string) $request->getBody(); + if (!empty($body)) { + $body = ' '.$body; + } + + return $request->getMethod().' '.$request->getUri().$body; + } +} diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 10b1c25..fd7d87f 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -4,6 +4,8 @@ use Http\Client\Common\Plugin; use Http\Client\Common\Plugin\Exception\RewindStreamException; +use Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator; +use Http\Client\Common\Plugin\Cache\Generator\SimpleGenerator; use Http\Message\StreamFactory; use Http\Promise\FulfilledPromise; use Psr\Cache\CacheItemInterface; @@ -55,7 +57,8 @@ 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 $respect_response_cache_directives list of cache directives this plugin will respect while caching responses. + * @var array $respect_response_cache_directives list of cache directives this plugin will respect while caching responses + * @var CacheKeyGenerator $cache_key_generator a class to generate the cache key. Defaults to SimpleGenerator * } */ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) @@ -73,6 +76,10 @@ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamF $optionsResolver = new OptionsResolver(); $this->configureOptions($optionsResolver); $this->config = $optionsResolver->resolve($config); + + if (null === $this->config['cache_key_generator']) { + $this->config['cache_key_generator'] = new SimpleGenerator(); + } } /** @@ -282,12 +289,9 @@ private function getCacheControlDirective(ResponseInterface $response, $name) */ private function createCacheKey(RequestInterface $request) { - $body = (string) $request->getBody(); - if (!empty($body)) { - $body = ' '.$body; - } + $key = $this->config['cache_key_generator']->generate($request); - return hash($this->config['hash_algo'], $request->getMethod().' '.$request->getUri().$body); + return hash($this->config['hash_algo'], $key); } /** @@ -338,12 +342,14 @@ private function configureOptions(OptionsResolver $resolver) 'hash_algo' => 'sha1', 'methods' => ['GET', 'HEAD'], 'respect_response_cache_directives' => ['no-cache', 'private', 'max-age', 'no-store'], + 'cache_key_generator' => null, ]); $resolver->setAllowedTypes('cache_lifetime', ['int', 'null']); $resolver->setAllowedTypes('default_ttl', ['int', 'null']); $resolver->setAllowedTypes('respect_cache_headers', 'bool'); $resolver->setAllowedTypes('methods', 'array'); + $resolver->setAllowedTypes('cache_key_generator', ['null', 'Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator']); $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. */