Skip to content

Commit

Permalink
Adding cache listener (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
iainconnor authored and dbu committed Jan 29, 2018
1 parent c573ac6 commit 5ae8a80
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 7 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.5-dev"
"dev-master": "1.6-dev"
}
}
}
42 changes: 42 additions & 0 deletions src/Cache/Listener/AddHeaderCacheListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Http\Client\Common\Plugin\Cache\Listener;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Cache\CacheItemInterface;

/**
* Adds a header indicating if the response came from cache.
*
* @author Iain Connor <iain.connor@priceline.com>
*/
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');
}
}
30 changes: 30 additions & 0 deletions src/Cache/Listener/CacheListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Http\Client\Common\Plugin\Cache\Listener;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Cache\CacheItemInterface;

/**
* Called by the cache plugin with information on the cache status.
* Provides an opportunity to update the response based on whether the cache was a hit or a miss, or
* other cache-meta-data.
*
* @author Iain Connor <iain.connor@priceline.com>
*/
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);
}
43 changes: 37 additions & 6 deletions src/CachePlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = [])
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)) {
Expand All @@ -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);
});
}

Expand Down Expand Up @@ -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']);
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
}

0 comments on commit 5ae8a80

Please sign in to comment.