Skip to content

Commit 1568bea

Browse files
committed
Implement caching types for client and server caching. Fixes #3
1 parent c8dfcc5 commit 1568bea

File tree

3 files changed

+140
-7
lines changed

3 files changed

+140
-7
lines changed

Diff for: CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44
### Added
55

66
- New `methods` setting which allows to configure the request methods which can be cached.
7+
- New `respect_response_cache_directives` config setting to define specific cache directives to respect when handling responses.
8+
- Introduced `CachePlugin::clientCache` and `CachePlugin::serverCache` factory methods to easily setup the plugin with
9+
the correct config settigns for each usecase.
10+
11+
### Changed
12+
13+
- The `no-cache` directive is now respected by the plugin and will not cache the response
14+
15+
### Deprecated
16+
17+
- The `respect_cache_headers` option is deprecated and will be removed in 2.0. This option is replaced by the new `respect_response_cache_directives` option.
718

819
## 1.2.0 - 2016-08-16
920

Diff for: spec/CachePluginSpec.php

+48
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,54 @@ function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, Ca
351351
$this->handleRequest($request, $next, function () {});
352352
}
353353

354+
function it_caches_private_responses_when_allowed(
355+
CacheItemPoolInterface $pool,
356+
CacheItemInterface $item,
357+
RequestInterface $request,
358+
ResponseInterface $response,
359+
StreamFactory $streamFactory,
360+
StreamInterface $stream
361+
) {
362+
$this->beConstructedThrough('clientCache', [$pool, $streamFactory, [
363+
'default_ttl' => 60,
364+
'cache_lifetime' => 1000,
365+
]]);
366+
367+
$httpBody = 'body';
368+
$stream->__toString()->willReturn($httpBody);
369+
$stream->isSeekable()->willReturn(true);
370+
$stream->rewind()->shouldBeCalled();
371+
372+
$request->getMethod()->willReturn('GET');
373+
$request->getUri()->willReturn('/');
374+
$request->getBody()->shouldBeCalled();
375+
376+
$response->getStatusCode()->willReturn(200);
377+
$response->getBody()->willReturn($stream);
378+
$response->getHeader('Cache-Control')->willReturn(['private'])->shouldBeCalled();
379+
$response->getHeader('Expires')->willReturn(array())->shouldBeCalled();
380+
$response->getHeader('ETag')->willReturn(array())->shouldBeCalled();
381+
382+
$pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item);
383+
$item->isHit()->willReturn(false);
384+
$item->expiresAfter(1060)->willReturn($item)->shouldBeCalled();
385+
386+
$item->set($this->getCacheItemMatcher([
387+
'response' => $response->getWrappedObject(),
388+
'body' => $httpBody,
389+
'expiresAt' => 0,
390+
'createdAt' => 0,
391+
'etag' => []
392+
]))->willReturn($item)->shouldBeCalled();
393+
$pool->save(Argument::any())->shouldBeCalled();
394+
395+
$next = function (RequestInterface $request) use ($response) {
396+
return new FulfilledPromise($response->getWrappedObject());
397+
};
398+
399+
$this->handleRequest($request, $next, function () {});
400+
}
401+
354402

355403
/**
356404
* Private function to match cache item data.

Diff for: src/CachePlugin.php

+81-7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Psr\Cache\CacheItemPoolInterface;
1010
use Psr\Http\Message\RequestInterface;
1111
use Psr\Http\Message\ResponseInterface;
12+
use Symfony\Component\OptionsResolver\Options;
1213
use Symfony\Component\OptionsResolver\OptionsResolver;
1314

1415
/**
@@ -33,6 +34,13 @@ final class CachePlugin implements Plugin
3334
*/
3435
private $config;
3536

37+
/**
38+
* Cache directives indicating if a response can not be cached.
39+
*
40+
* @var array
41+
*/
42+
private $noCacheFlags = ['no-cache', 'private', 'no-store'];
43+
3644
/**
3745
* @param CacheItemPoolInterface $pool
3846
* @param StreamFactory $streamFactory
@@ -45,19 +53,66 @@ final class CachePlugin implements Plugin
4553
* @var int $cache_lifetime (seconds) To support serving a previous stale response when the server answers 304
4654
* we have to store the cache for a longer time than the server originally says it is valid for.
4755
* We store a cache item for $cache_lifetime + max age of the response.
48-
* @var array $methods list of request methods which can be cached.
56+
* @var array $methods list of request methods which can be cached
57+
* @var array $respect_response_cache_directives list of cache directives this plugin will respect while caching responses.
4958
* }
5059
*/
5160
public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = [])
5261
{
5362
$this->pool = $pool;
5463
$this->streamFactory = $streamFactory;
5564

65+
if (isset($config['respect_cache_headers']) && isset($config['respect_response_cache_directives'])) {
66+
throw new \InvalidArgumentException(
67+
'You can\'t provide config option "respect_cache_headers" and "respect_response_cache_directives". '.
68+
'Use "respect_response_cache_directives" instead.'
69+
);
70+
}
71+
5672
$optionsResolver = new OptionsResolver();
5773
$this->configureOptions($optionsResolver);
5874
$this->config = $optionsResolver->resolve($config);
5975
}
6076

77+
/**
78+
* This method will setup the cachePlugin in client cache mode. When using the client cache mode the plugin will
79+
* cache responses with `private` cache directive.
80+
*
81+
* @param CacheItemPoolInterface $pool
82+
* @param StreamFactory $streamFactory
83+
* @param array $config For all possible config options see the constructor docs
84+
*
85+
* @return CachePlugin
86+
*/
87+
public static function clientCache(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = [])
88+
{
89+
// Allow caching of private requests
90+
if (isset($config['respect_response_cache_directives'])) {
91+
$config['respect_response_cache_directives'][] = 'no-cache';
92+
$config['respect_response_cache_directives'][] = 'max-age';
93+
$config['respect_response_cache_directives'] = array_unique($config['respect_response_cache_directives']);
94+
} else {
95+
$config['respect_response_cache_directives'] = ['no-cache', 'max-age'];
96+
}
97+
98+
return new self($pool, $streamFactory, $config);
99+
}
100+
101+
/**
102+
* This method will setup the cachePlugin in server cache mode. This is the default caching behavior it refuses to
103+
* cache responses with the `private`or `no-cache` directives.
104+
*
105+
* @param CacheItemPoolInterface $pool
106+
* @param StreamFactory $streamFactory
107+
* @param array $config For all possible config options see the constructor docs
108+
*
109+
* @return CachePlugin
110+
*/
111+
public static function serverCache(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = [])
112+
{
113+
return new self($pool, $streamFactory, $config);
114+
}
115+
61116
/**
62117
* {@inheritdoc}
63118
*/
@@ -183,11 +238,12 @@ protected function isCacheable(ResponseInterface $response)
183238
if (!in_array($response->getStatusCode(), [200, 203, 300, 301, 302, 404, 410])) {
184239
return false;
185240
}
186-
if (!$this->config['respect_cache_headers']) {
187-
return true;
188-
}
189-
if ($this->getCacheControlDirective($response, 'no-store') || $this->getCacheControlDirective($response, 'private')) {
190-
return false;
241+
242+
$nocacheDirectives = array_intersect($this->config['respect_response_cache_directives'], $this->noCacheFlags);
243+
foreach ($nocacheDirectives as $nocacheDirective) {
244+
if ($this->getCacheControlDirective($response, $nocacheDirective)) {
245+
return false;
246+
}
191247
}
192248

193249
return true;
@@ -242,7 +298,7 @@ private function createCacheKey(RequestInterface $request)
242298
*/
243299
private function getMaxAge(ResponseInterface $response)
244300
{
245-
if (!$this->config['respect_cache_headers']) {
301+
if (!in_array('max-age', $this->config['respect_response_cache_directives'], true)) {
246302
return $this->config['default_ttl'];
247303
}
248304

@@ -276,9 +332,11 @@ private function configureOptions(OptionsResolver $resolver)
276332
$resolver->setDefaults([
277333
'cache_lifetime' => 86400 * 30, // 30 days
278334
'default_ttl' => 0,
335+
//Deprecated as of v1.3, to be removed in v2.0. Use respect_response_cache_directives instead
279336
'respect_cache_headers' => true,
280337
'hash_algo' => 'sha1',
281338
'methods' => ['GET', 'HEAD'],
339+
'respect_response_cache_directives' => ['no-cache', 'private', 'max-age', 'no-store'],
282340
]);
283341

284342
$resolver->setAllowedTypes('cache_lifetime', ['int', 'null']);
@@ -292,6 +350,22 @@ private function configureOptions(OptionsResolver $resolver)
292350

293351
return empty($matches);
294352
});
353+
354+
$resolver->setNormalizer('respect_cache_headers', function (Options $options, $value) {
355+
if (null !== $value) {
356+
@trigger_error('The option "respect_cache_headers" is deprecated since version 1.3 and will be removed in 2.0. Use "respect_response_cache_directives" instead.', E_USER_DEPRECATED);
357+
}
358+
359+
return $value;
360+
});
361+
362+
$resolver->setNormalizer('respect_response_cache_directives', function (Options $options, $value) {
363+
if (false === $options['respect_cache_headers']) {
364+
return [];
365+
}
366+
367+
return $value;
368+
});
295369
}
296370

297371
/**

0 commit comments

Comments
 (0)