diff --git a/composer.json b/composer.json index d965b15..012381d 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,9 @@ "require": { "php": ">=5.4", "php-http/httplug": "1.0.0-beta", - "php-http/client-tools": "^0.1@dev" + "php-http/client-tools": "^0.1@dev", + "php-http/message-factory": "^1.0", + "symfony/options-resolver": "^2.6|^3.0" }, "require-dev": { "phpspec/phpspec": "^2.4-alpha", diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index 2b84c33..36f19e7 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -3,17 +3,19 @@ namespace spec\Http\Client\Plugin; use Http\Client\Tools\Promise\FulfilledPromise; +use Http\Message\StreamFactory; use PhpSpec\ObjectBehavior; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; class CachePluginSpec extends ObjectBehavior { - function let(CacheItemPoolInterface $pool) + function let(CacheItemPoolInterface $pool, StreamFactory $streamFactory) { - $this->beConstructedWith($pool, ['default_ttl'=>60]); + $this->beConstructedWith($pool, $streamFactory, ['default_ttl'=>60]); } function it_is_initializable(CacheItemPoolInterface $pool) @@ -26,17 +28,23 @@ function it_is_a_plugin() $this->shouldImplement('Http\Client\Plugin\Plugin'); } - function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response) + function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream) { + $httpBody = 'body'; + $stream->__toString()->willReturn($httpBody); + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn('/'); $response->getStatusCode()->willReturn(200); + $response->getBody()->willReturn($stream); $response->getHeader('Cache-Control')->willReturn(array()); $response->getHeader('Expires')->willReturn(array()); $pool->getItem('e3b717d5883a45ef9493d009741f7c64')->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); - $item->set($response)->willReturn($item)->shouldBeCalled(); + $item->set(['response' => $response, 'body' => $httpBody])->willReturn($item)->shouldBeCalled(); $item->expiresAfter(60)->willReturn($item)->shouldBeCalled(); $pool->save($item)->shouldBeCalled(); @@ -78,11 +86,17 @@ function it_doesnt_store_post_requests(CacheItemPoolInterface $pool, CacheItemIn } - function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response) + function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream) { + $httpBody = 'body'; + $stream->__toString()->willReturn($httpBody); + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn('/'); $response->getStatusCode()->willReturn(200); + $response->getBody()->willReturn($stream); $response->getHeader('Cache-Control')->willReturn(array('max-age=40')); $response->getHeader('Age')->willReturn(array('15')); $response->getHeader('Expires')->willReturn(array()); @@ -91,7 +105,7 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI $item->isHit()->willReturn(false); // 40-15 should be 25 - $item->set($response)->willReturn($item)->shouldBeCalled(); + $item->set(['response' => $response, 'body' => $httpBody])->willReturn($item)->shouldBeCalled(); $item->expiresAfter(25)->willReturn($item)->shouldBeCalled(); $pool->save($item)->shouldBeCalled(); diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 4e07342..6d1bb9c 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -3,9 +3,11 @@ namespace Http\Client\Plugin; use Http\Client\Tools\Promise\FulfilledPromise; +use Http\Message\StreamFactory; use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; /** * Allow for caching a response. @@ -20,33 +22,32 @@ class CachePlugin implements Plugin private $pool; /** - * Default time to store object in cache. This value is used if CachePlugin::respectCacheHeaders is false or - * if cache headers are missing. - * - * @var int + * @var StreamFactory */ - private $defaultTtl; + private $streamFactory; /** - * Look at the cache headers to know whether this response may be cached and to - * decide how it can be cached. - * - * @var bool Defaults to true + * @var array */ - private $respectCacheHeaders; + private $config; /** * Available options are * - respect_cache_headers: Whether to look at the cache directives or ignore them. - * + * - default_ttl: If we do not respect cache headers or can't calculate a good ttl, use this value. + * * @param CacheItemPoolInterface $pool - * @param array $options + * @param StreamFactory $streamFactory + * @param array $config */ - public function __construct(CacheItemPoolInterface $pool, array $options = []) + public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) { $this->pool = $pool; - $this->defaultTtl = isset($options['default_ttl']) ? $options['default_ttl'] : null; - $this->respectCacheHeaders = isset($options['respect_cache_headers']) ? $options['respect_cache_headers'] : true; + $this->streamFactory = $streamFactory; + + $optionsResolver = new OptionsResolver(); + $this->configureOptions($optionsResolver); + $this->config = $optionsResolver->resolve($config); } /** @@ -67,12 +68,24 @@ public function handleRequest(RequestInterface $request, callable $next, callabl if ($cacheItem->isHit()) { // return cached response - return new FulfilledPromise($cacheItem->get()); + $data = $cacheItem->get(); + $response = $data['response']; + $response = $response->withBody($this->streamFactory->createStream($data['body'])); + + return new FulfilledPromise($response); } return $next($request)->then(function (ResponseInterface $response) use ($cacheItem) { if ($this->isCacheable($response)) { - $cacheItem->set($response) + $bodyStream = $response->getBody(); + $body = $bodyStream->__toString(); + if ($bodyStream->isSeekable()) { + $bodyStream->rewind(); + } else { + $response = $response->withBody($this->streamFactory->createStream($body)); + } + + $cacheItem->set(['response' => $response, 'body' => $body]) ->expiresAfter($this->getMaxAge($response)); $this->pool->save($cacheItem); } @@ -93,7 +106,7 @@ protected function isCacheable(ResponseInterface $response) if (!in_array($response->getStatusCode(), [200, 203, 300, 301, 302, 404, 410])) { return false; } - if (!$this->respectCacheHeaders) { + if (!$this->config['respect_cache_headers']) { return true; } if ($this->getCacheControlDirective($response, 'no-store') || $this->getCacheControlDirective($response, 'private')) { @@ -148,8 +161,8 @@ private function createCacheKey(RequestInterface $request) */ private function getMaxAge(ResponseInterface $response) { - if (!$this->respectCacheHeaders) { - return $this->defaultTtl; + if (!$this->config['respect_cache_headers']) { + return $this->config['default_ttl']; } // check for max age in the Cache-Control header @@ -169,6 +182,22 @@ private function getMaxAge(ResponseInterface $response) return (new \DateTime($header))->getTimestamp() - (new \DateTime())->getTimestamp(); } - return $this->defaultTtl; + return $this->config['default_ttl']; + } + + /** + * Configure an options resolver. + * + * @param OptionsResolver $resolver + */ + private function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'default_ttl' => null, + 'respect_cache_headers' => true, + ]); + + $resolver->setAllowedTypes('default_ttl', ['int', 'null']); + $resolver->setAllowedTypes('respect_cache_headers', 'bool'); } }