From 5ae8a805966987a12f5185897914c2966acdf09e Mon Sep 17 00:00:00 2001 From: Iain Connor Date: Mon, 29 Jan 2018 10:52:48 -0600 Subject: [PATCH] Adding cache listener (#48) --- CHANGELOG.md | 6 +++ composer.json | 2 +- src/Cache/Listener/AddHeaderCacheListener.php | 42 ++++++++++++++++++ src/Cache/Listener/CacheListener.php | 30 +++++++++++++ src/CachePlugin.php | 43 ++++++++++++++++--- 5 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 src/Cache/Listener/AddHeaderCacheListener.php create mode 100644 src/Cache/Listener/CacheListener.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 795a3ff..e822f6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 1.6.0 - 2017-01-29 + +### Added + +* Added `cache_listeners` option, which takes an array of `CacheListener`s, who get notified and can optionally act on a Response based on a cache hit or miss event. An implementation, `AddHeaderCacheListener`, is provided which will add an `X-Cache` header to the response with this information. + ## 1.5.0 - 2017-11-29 ### Added diff --git a/composer.json b/composer.json index 3ce18be..d875cdb 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.5-dev" + "dev-master": "1.6-dev" } } } diff --git a/src/Cache/Listener/AddHeaderCacheListener.php b/src/Cache/Listener/AddHeaderCacheListener.php new file mode 100644 index 0000000..ed306e3 --- /dev/null +++ b/src/Cache/Listener/AddHeaderCacheListener.php @@ -0,0 +1,42 @@ + + */ +class AddHeaderCacheListener implements CacheListener +{ + /** @var string */ + private $headerName; + + /** + * @param string $headerName + */ + public function __construct($headerName = 'X-Cache') + { + $this->headerName = $headerName; + } + + /** + * Called before the cache plugin returns the response, with information on whether that response came from cache. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param bool $fromCache Whether the `$response` was from the cache or not. + * Note that checking `$cacheItem->isHit()` is not sufficent to determine this. + * @param CacheItemInterface|null $cacheItem + * + * @return ResponseInterface + */ + public function onCacheResponse(RequestInterface $request, ResponseInterface $response, $fromCache, $cacheItem) + { + return $response->withHeader($this->headerName, $fromCache ? 'HIT' : 'MISS'); + } +} diff --git a/src/Cache/Listener/CacheListener.php b/src/Cache/Listener/CacheListener.php new file mode 100644 index 0000000..3bf6007 --- /dev/null +++ b/src/Cache/Listener/CacheListener.php @@ -0,0 +1,30 @@ + + */ +interface CacheListener +{ + /** + * Called before the cache plugin returns the response, with information on whether that response came from cache. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param bool $fromCache Whether the `$response` was from the cache or not. + * Note that checking `$cacheItem->isHit()` is not sufficent to determine this. + * @param CacheItemInterface|null $cacheItem + * + * @return ResponseInterface + */ + public function onCacheResponse(RequestInterface $request, ResponseInterface $response, $fromCache, $cacheItem); +} diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 166cf9b..8225b75 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -6,6 +6,7 @@ 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\Client\Common\Plugin\Cache\Listener\CacheListener; use Http\Message\StreamFactory; use Http\Promise\FulfilledPromise; use Psr\Cache\CacheItemInterface; @@ -59,6 +60,8 @@ final class CachePlugin implements Plugin * @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 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. + * Defaults to an empty array * } */ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) @@ -129,7 +132,11 @@ public function handleRequest(RequestInterface $request, callable $next, callabl $method = strtoupper($request->getMethod()); // if the request not is cachable, move to $next if (!in_array($method, $this->config['methods'])) { - return $next($request); + return $next($request)->then(function (ResponseInterface $response) use ($request) { + $response = $this->handleCacheListeners($request, $response, false, null); + + return $response; + }); } // If we can cache the request @@ -141,7 +148,10 @@ public function handleRequest(RequestInterface $request, callable $next, callabl // The array_key_exists() is to be removed in 2.0. if (array_key_exists('expiresAt', $data) && (null === $data['expiresAt'] || time() < $data['expiresAt'])) { // This item is still valid according to previous cache headers - return new FulfilledPromise($this->createResponseFromCacheItem($cacheItem)); + $response = $this->createResponseFromCacheItem($cacheItem); + $response = $this->handleCacheListeners($request, $response, true, $cacheItem); + + return new FulfilledPromise($response); } // Add headers to ask the server if this cache is still valid @@ -154,14 +164,14 @@ public function handleRequest(RequestInterface $request, callable $next, callabl } } - return $next($request)->then(function (ResponseInterface $response) use ($cacheItem) { + return $next($request)->then(function (ResponseInterface $response) use ($request, $cacheItem) { if (304 === $response->getStatusCode()) { if (!$cacheItem->isHit()) { /* * We do not have the item in cache. This plugin did not add If-Modified-Since * or If-None-Match headers. Return the response from server. */ - return $response; + return $this->handleCacheListeners($request, $response, false, $cacheItem); } // The cached response we have is still valid @@ -171,7 +181,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl $cacheItem->set($data)->expiresAfter($this->calculateCacheItemExpiresAfter($maxAge)); $this->pool->save($cacheItem); - return $this->createResponseFromCacheItem($cacheItem); + return $this->handleCacheListeners($request, $this->createResponseFromCacheItem($cacheItem), true, $cacheItem); } if ($this->isCacheable($response)) { @@ -196,7 +206,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl $this->pool->save($cacheItem); } - return $response; + return $this->handleCacheListeners($request, $response, false, isset($cacheItem) ? $cacheItem : null); }); } @@ -343,6 +353,7 @@ private function configureOptions(OptionsResolver $resolver) 'methods' => ['GET', 'HEAD'], 'respect_response_cache_directives' => ['no-cache', 'private', 'max-age', 'no-store'], 'cache_key_generator' => null, + 'cache_listeners' => [], ]); $resolver->setAllowedTypes('cache_lifetime', ['int', 'null']); @@ -357,6 +368,7 @@ private function configureOptions(OptionsResolver $resolver) return empty($matches); }); + $resolver->setAllowedTypes('cache_listeners', ['array']); $resolver->setNormalizer('respect_cache_headers', function (Options $options, $value) { if (null !== $value) { @@ -441,4 +453,23 @@ private function getETag(CacheItemInterface $cacheItem) } } } + + /** + * Call the cache listeners, if they are set. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param bool $cacheHit + * @param CacheItemInterface|null $cacheItem + * + * @return ResponseInterface + */ + private function handleCacheListeners(RequestInterface $request, ResponseInterface $response, $cacheHit, $cacheItem) + { + foreach ($this->config['cache_listeners'] as $cacheListener) { + $response = $cacheListener->onCacheResponse($request, $response, $cacheHit, $cacheItem); + } + + return $response; + } }