Skip to content

Optionally enable caching of any request method #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Feb 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Change Log

## 1.3.0 - unreleased
### Added

- New `methods` setting which allows to configure the request methods which can be cached.

## 1.2.0 - 2016-08-16

### Changed

- The default value for `default_ttl` is changed from `null` to `0`.
- The default value for `default_ttl` is changed from `null` to `0`.

### Fixed

Expand Down
80 changes: 79 additions & 1 deletion spec/CachePluginSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i

$request->getMethod()->willReturn('GET');
$request->getUri()->willReturn('/');
$request->getBody()->shouldBeCalled();

$response->getStatusCode()->willReturn(200);
$response->getBody()->willReturn($stream);
$response->getHeader('Cache-Control')->willReturn(array())->shouldBeCalled();
Expand Down Expand Up @@ -71,6 +73,8 @@ function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheIte
{
$request->getMethod()->willReturn('GET');
$request->getUri()->willReturn('/');
$request->getBody()->shouldBeCalled();

$response->getStatusCode()->willReturn(400);
$response->getHeader('Cache-Control')->willReturn(array());
$response->getHeader('Expires')->willReturn(array());
Expand All @@ -85,7 +89,7 @@ function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheIte
$this->handleRequest($request, $next, function () {});
}

function it_doesnt_store_post_requests(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response)
function it_doesnt_store_post_requests_by_default(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response)
{
$request->getMethod()->willReturn('POST');
$request->getUri()->willReturn('/');
Expand All @@ -97,6 +101,74 @@ function it_doesnt_store_post_requests(CacheItemPoolInterface $pool, CacheItemIn
$this->handleRequest($request, $next, function () {});
}

function it_stores_post_requests_when_allowed(
CacheItemPoolInterface $pool,
CacheItemInterface $item,
RequestInterface $request,
ResponseInterface $response,
StreamFactory $streamFactory,
StreamInterface $stream
) {
$this->beConstructedWith($pool, $streamFactory, [
'default_ttl' => 60,
'cache_lifetime' => 1000,
'methods' => ['GET', 'HEAD', 'POST']
]);

$httpBody = 'hello=world';
$stream->__toString()->willReturn($httpBody);
$stream->isSeekable()->willReturn(true);
$stream->rewind()->shouldBeCalled();

$request->getMethod()->willReturn('POST');
$request->getUri()->willReturn('/post');
$request->getBody()->willReturn($stream);

$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('e4311a9af932c603b400a54efab21b6d7dea7a90')->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_does_not_allow_invalid_request_methods(
CacheItemPoolInterface $pool,
CacheItemInterface $item,
RequestInterface $request,
ResponseInterface $response,
StreamFactory $streamFactory,
StreamInterface $stream
) {
$this
->shouldThrow("Symfony\Component\OptionsResolver\Exception\InvalidOptionsException")
->during('__construct', [$pool, $streamFactory, ['methods' => ['GET', 'HEAD', 'POST ']]]);
$this
->shouldThrow("Symfony\Component\OptionsResolver\Exception\InvalidOptionsException")
->during('__construct', [$pool, $streamFactory, ['methods' => ['GET', 'HEAD"', 'POST']]]);
$this
->shouldThrow("Symfony\Component\OptionsResolver\Exception\InvalidOptionsException")
->during('__construct', [$pool, $streamFactory, ['methods' => ['GET', 'head', 'POST']]]);
}

function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream)
{
Expand All @@ -107,6 +179,8 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI

$request->getMethod()->willReturn('GET');
$request->getUri()->willReturn('/');
$request->getBody()->shouldBeCalled();

$response->getStatusCode()->willReturn(200);
$response->getBody()->willReturn($stream);
$response->getHeader('Cache-Control')->willReturn(array('max-age=40'));
Expand Down Expand Up @@ -141,6 +215,7 @@ function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, R
$stream->__toString()->willReturn($httpBody);
$stream->isSeekable()->willReturn(true);
$stream->rewind()->shouldBeCalled();
$request->getBody()->shouldBeCalled();

$request->getMethod()->willReturn('GET');
$request->getUri()->willReturn('/');
Expand Down Expand Up @@ -176,6 +251,7 @@ function it_adds_etag_and_modfied_since_to_request(CacheItemPoolInterface $pool,

$request->getMethod()->willReturn('GET');
$request->getUri()->willReturn('/');
$request->getBody()->shouldBeCalled();

$request->withHeader('If-Modified-Since', 'Thursday, 01-Jan-70 01:18:31 GMT')->shouldBeCalled()->willReturn($request);
$request->withHeader('If-None-Match', 'foo_etag')->shouldBeCalled()->willReturn($request);
Expand Down Expand Up @@ -205,6 +281,7 @@ function it_servces_a_cached_response(CacheItemPoolInterface $pool, CacheItemInt

$request->getMethod()->willReturn('GET');
$request->getUri()->willReturn('/');
$request->getBody()->shouldBeCalled();

$pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item);
$item->isHit()->willReturn(true);
Expand Down Expand Up @@ -233,6 +310,7 @@ function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, Ca

$request->getMethod()->willReturn('GET');
$request->getUri()->willReturn('/');
$request->getBody()->shouldBeCalled();

$request->withHeader(Argument::any(), Argument::any())->willReturn($request);
$request->withHeader(Argument::any(), Argument::any())->willReturn($request);
Expand Down
19 changes: 16 additions & 3 deletions src/CachePlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ final class CachePlugin implements Plugin
* @var int $cache_lifetime (seconds) To support serving a previous stale response when the server answers 304
* 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.
* }
*/
public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = [])
Expand All @@ -64,7 +65,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
{
$method = strtoupper($request->getMethod());
// if the request not is cachable, move to $next
if ($method !== 'GET' && $method !== 'HEAD') {
if (!in_array($method, $this->config['methods'])) {
return $next($request);
}

Expand Down Expand Up @@ -205,7 +206,6 @@ private function getCacheControlDirective(ResponseInterface $response, $name)
$headers = $response->getHeader('Cache-Control');
foreach ($headers as $header) {
if (preg_match(sprintf('|%s=?([0-9]+)?|i', $name), $header, $matches)) {

// return the value for $name if it exists
if (isset($matches[1])) {
return $matches[1];
Expand All @@ -225,7 +225,12 @@ private function getCacheControlDirective(ResponseInterface $response, $name)
*/
private function createCacheKey(RequestInterface $request)
{
return hash($this->config['hash_algo'], $request->getMethod().' '.$request->getUri());
$body = (string) $request->getBody();
if (!empty($body)) {
$body = ' '.$body;
}

return hash($this->config['hash_algo'], $request->getMethod().' '.$request->getUri().$body);
}

/**
Expand Down Expand Up @@ -273,12 +278,20 @@ private function configureOptions(OptionsResolver $resolver)
'default_ttl' => 0,
'respect_cache_headers' => true,
'hash_algo' => 'sha1',
'methods' => ['GET', 'HEAD'],
]);

$resolver->setAllowedTypes('cache_lifetime', ['int', 'null']);
$resolver->setAllowedTypes('default_ttl', ['int', 'null']);
$resolver->setAllowedTypes('respect_cache_headers', 'bool');
$resolver->setAllowedTypes('methods', '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. */
$matches = preg_grep('/[^A-Z0-9!#$%&\'*\/+\-.^_`|~]/', $value);

return empty($matches);
});
}

/**
Expand Down