1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Guzzle\Plugin\Cache; |
4
|
|
|
|
5
|
|
|
use Guzzle\Cache\CacheAdapterFactory; |
6
|
|
|
use Guzzle\Cache\CacheAdapterInterface; |
7
|
|
|
use Guzzle\Common\Event; |
8
|
|
|
use Guzzle\Common\Exception\InvalidArgumentException; |
9
|
|
|
use Guzzle\Common\Version; |
10
|
|
|
use Guzzle\Http\Message\RequestInterface; |
11
|
|
|
use Guzzle\Http\Message\Response; |
12
|
|
|
use Guzzle\Cache\DoctrineCacheAdapter; |
13
|
|
|
use Guzzle\Http\Exception\CurlException; |
14
|
|
|
use Doctrine\Common\Cache\ArrayCache; |
15
|
|
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Plugin to enable the caching of GET and HEAD requests. Caching can be done on all requests passing through this |
19
|
|
|
* plugin or only after retrieving resources with cacheable response headers. |
20
|
|
|
* |
21
|
|
|
* This is a simple implementation of RFC 2616 and should be considered a private transparent proxy cache, meaning |
22
|
|
|
* authorization and private data can be cached. |
23
|
|
|
* |
24
|
|
|
* It also implements RFC 5861's `stale-if-error` Cache-Control extension, allowing stale cache responses to be used |
25
|
|
|
* when an error is encountered (such as a `500 Internal Server Error` or DNS failure). |
26
|
|
|
*/ |
27
|
|
|
class CachePlugin implements EventSubscriberInterface |
28
|
|
|
{ |
29
|
|
|
/** @var RevalidationInterface Cache revalidation strategy */ |
30
|
|
|
protected $revalidation; |
31
|
|
|
|
32
|
|
|
/** @var CanCacheStrategyInterface Object used to determine if a request can be cached */ |
33
|
|
|
protected $canCache; |
34
|
|
|
|
35
|
|
|
/** @var CacheStorageInterface $cache Object used to cache responses */ |
36
|
|
|
protected $storage; |
37
|
|
|
|
38
|
|
|
/** @var bool */ |
39
|
|
|
protected $autoPurge; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @param array|CacheAdapterInterface|CacheStorageInterface $options Array of options for the cache plugin, |
43
|
|
|
* cache adapter, or cache storage object. |
44
|
|
|
* - CacheStorageInterface storage: Adapter used to cache responses |
45
|
|
|
* - RevalidationInterface revalidation: Cache revalidation strategy |
46
|
|
|
* - CanCacheInterface can_cache: Object used to determine if a request can be cached |
47
|
|
|
* - bool auto_purge Set to true to automatically PURGE resources when non-idempotent |
48
|
|
|
* requests are sent to a resource. Defaults to false. |
49
|
|
|
* @throws InvalidArgumentException if no cache is provided and Doctrine cache is not installed |
50
|
|
|
*/ |
51
|
|
|
public function __construct($options = null) |
52
|
|
|
{ |
53
|
|
|
if (!is_array($options)) { |
54
|
|
|
if ($options instanceof CacheAdapterInterface) { |
55
|
|
|
$options = array('storage' => new DefaultCacheStorage($options)); |
56
|
|
|
} elseif ($options instanceof CacheStorageInterface) { |
57
|
|
|
$options = array('storage' => $options); |
58
|
|
|
} elseif ($options) { |
59
|
|
|
$options = array('storage' => new DefaultCacheStorage(CacheAdapterFactory::fromCache($options))); |
60
|
|
|
} elseif (!class_exists('Doctrine\Common\Cache\ArrayCache')) { |
61
|
|
|
// @codeCoverageIgnoreStart |
62
|
|
|
throw new InvalidArgumentException('No cache was provided and Doctrine is not installed'); |
63
|
|
|
// @codeCoverageIgnoreEnd |
64
|
|
|
} |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
$this->autoPurge = isset($options['auto_purge']) ? $options['auto_purge'] : false; |
68
|
|
|
|
69
|
|
|
// Add a cache storage if a cache adapter was provided |
70
|
|
|
$this->storage = isset($options['storage']) |
71
|
|
|
? $options['storage'] |
72
|
|
|
: new DefaultCacheStorage(new DoctrineCacheAdapter(new ArrayCache())); |
73
|
|
|
|
74
|
|
|
if (!isset($options['can_cache'])) { |
75
|
|
|
$this->canCache = new DefaultCanCacheStrategy(); |
76
|
|
|
} else { |
77
|
|
|
$this->canCache = is_callable($options['can_cache']) |
78
|
|
|
? new CallbackCanCacheStrategy($options['can_cache']) |
79
|
|
|
: $options['can_cache']; |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
// Use the provided revalidation strategy or the default |
83
|
|
|
$this->revalidation = isset($options['revalidation']) |
84
|
|
|
? $options['revalidation'] |
85
|
|
|
: new DefaultRevalidation($this->storage, $this->canCache); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
public static function getSubscribedEvents() |
89
|
|
|
{ |
90
|
|
|
return array( |
91
|
|
|
'request.before_send' => array('onRequestBeforeSend', -255), |
92
|
|
|
'request.sent' => array('onRequestSent', 255), |
93
|
|
|
'request.error' => array('onRequestError', 0), |
94
|
|
|
'request.exception' => array('onRequestException', 0), |
95
|
|
|
); |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Check if a response in cache will satisfy the request before sending |
100
|
|
|
* |
101
|
|
|
* @param Event $event |
102
|
|
|
*/ |
103
|
|
|
public function onRequestBeforeSend(Event $event) |
104
|
|
|
{ |
105
|
|
|
$request = $event['request']; |
106
|
|
|
$request->addHeader('Via', sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION)); |
107
|
|
|
|
108
|
|
|
if (!$this->canCache->canCacheRequest($request)) { |
109
|
|
|
switch ($request->getMethod()) { |
110
|
|
|
case 'PURGE': |
111
|
|
|
$this->purge($request); |
112
|
|
|
$request->setResponse(new Response(200, array(), 'purged')); |
113
|
|
|
break; |
114
|
|
|
case 'PUT': |
115
|
|
|
case 'POST': |
116
|
|
|
case 'DELETE': |
117
|
|
|
case 'PATCH': |
118
|
|
|
if ($this->autoPurge) { |
119
|
|
|
$this->purge($request); |
120
|
|
|
} |
121
|
|
|
} |
122
|
|
|
return; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
if ($response = $this->storage->fetch($request)) { |
126
|
|
|
$params = $request->getParams(); |
127
|
|
|
$params['cache.lookup'] = true; |
128
|
|
|
$response->setHeader( |
129
|
|
|
'Age', |
130
|
|
|
time() - strtotime($response->getDate() ? : $response->getLastModified() ?: 'now') |
131
|
|
|
); |
132
|
|
|
// Validate that the response satisfies the request |
133
|
|
|
if ($this->canResponseSatisfyRequest($request, $response)) { |
134
|
|
|
if (!isset($params['cache.hit'])) { |
135
|
|
|
$params['cache.hit'] = true; |
136
|
|
|
} |
137
|
|
|
$request->setResponse($response); |
138
|
|
|
} |
139
|
|
|
} |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* If possible, store a response in cache after sending |
144
|
|
|
* |
145
|
|
|
* @param Event $event |
146
|
|
|
*/ |
147
|
|
|
public function onRequestSent(Event $event) |
148
|
|
|
{ |
149
|
|
|
$request = $event['request']; |
150
|
|
|
$response = $event['response']; |
151
|
|
|
|
152
|
|
|
if ($request->getParams()->get('cache.hit') === null && |
153
|
|
|
$this->canCache->canCacheRequest($request) && |
154
|
|
|
$this->canCache->canCacheResponse($response) |
155
|
|
|
) { |
156
|
|
|
$this->storage->cache($request, $response); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
$this->addResponseHeaders($request, $response); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* If possible, return a cache response on an error |
164
|
|
|
* |
165
|
|
|
* @param Event $event |
166
|
|
|
*/ |
167
|
|
|
public function onRequestError(Event $event) |
168
|
|
|
{ |
169
|
|
|
$request = $event['request']; |
170
|
|
|
|
171
|
|
|
if (!$this->canCache->canCacheRequest($request)) { |
172
|
|
|
return; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
View Code Duplication |
if ($response = $this->storage->fetch($request)) { |
|
|
|
|
176
|
|
|
$response->setHeader( |
177
|
|
|
'Age', |
178
|
|
|
time() - strtotime($response->getLastModified() ? : $response->getDate() ?: 'now') |
179
|
|
|
); |
180
|
|
|
|
181
|
|
|
if ($this->canResponseSatisfyFailedRequest($request, $response)) { |
182
|
|
|
$request->getParams()->set('cache.hit', 'error'); |
183
|
|
|
$this->addResponseHeaders($request, $response); |
184
|
|
|
$event['response'] = $response; |
185
|
|
|
$event->stopPropagation(); |
186
|
|
|
} |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* If possible, set a cache response on a cURL exception |
192
|
|
|
* |
193
|
|
|
* @param Event $event |
194
|
|
|
* |
195
|
|
|
* @return null |
196
|
|
|
*/ |
197
|
|
|
public function onRequestException(Event $event) |
198
|
|
|
{ |
199
|
|
|
if (!$event['exception'] instanceof CurlException) { |
200
|
|
|
return; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
$request = $event['request']; |
204
|
|
|
if (!$this->canCache->canCacheRequest($request)) { |
205
|
|
|
return; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
View Code Duplication |
if ($response = $this->storage->fetch($request)) { |
|
|
|
|
209
|
|
|
$response->setHeader('Age', time() - strtotime($response->getDate() ? : 'now')); |
210
|
|
|
if (!$this->canResponseSatisfyFailedRequest($request, $response)) { |
211
|
|
|
return; |
212
|
|
|
} |
213
|
|
|
$request->getParams()->set('cache.hit', 'error'); |
214
|
|
|
$request->setResponse($response); |
215
|
|
|
$this->addResponseHeaders($request, $response); |
216
|
|
|
$event->stopPropagation(); |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Check if a cache response satisfies a request's caching constraints |
222
|
|
|
* |
223
|
|
|
* @param RequestInterface $request Request to validate |
224
|
|
|
* @param Response $response Response to validate |
225
|
|
|
* |
226
|
|
|
* @return bool |
227
|
|
|
*/ |
228
|
|
|
public function canResponseSatisfyRequest(RequestInterface $request, Response $response) |
229
|
|
|
{ |
230
|
|
|
$responseAge = $response->calculateAge(); |
231
|
|
|
$reqc = $request->getHeader('Cache-Control'); |
232
|
|
|
$resc = $response->getHeader('Cache-Control'); |
233
|
|
|
|
234
|
|
|
// Check the request's max-age header against the age of the response |
235
|
|
|
if ($reqc && $reqc->hasDirective('max-age') && |
236
|
|
|
$responseAge > $reqc->getDirective('max-age')) { |
237
|
|
|
return false; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
// Check the response's max-age header |
241
|
|
|
if ($response->isFresh() === false) { |
242
|
|
|
$maxStale = $reqc ? $reqc->getDirective('max-stale') : null; |
243
|
|
|
if (null !== $maxStale) { |
244
|
|
|
if ($maxStale !== true && $response->getFreshness() < (-1 * $maxStale)) { |
245
|
|
|
return false; |
246
|
|
|
} |
247
|
|
|
} elseif ($resc && $resc->hasDirective('max-age') |
248
|
|
|
&& $responseAge > $resc->getDirective('max-age') |
249
|
|
|
) { |
250
|
|
|
return false; |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
if ($this->revalidation->shouldRevalidate($request, $response)) { |
255
|
|
|
try { |
256
|
|
|
return $this->revalidation->revalidate($request, $response); |
257
|
|
|
} catch (CurlException $e) { |
258
|
|
|
$request->getParams()->set('cache.hit', 'error'); |
259
|
|
|
return $this->canResponseSatisfyFailedRequest($request, $response); |
260
|
|
|
} |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
return true; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Check if a cache response satisfies a failed request's caching constraints |
268
|
|
|
* |
269
|
|
|
* @param RequestInterface $request Request to validate |
270
|
|
|
* @param Response $response Response to validate |
271
|
|
|
* |
272
|
|
|
* @return bool |
273
|
|
|
*/ |
274
|
|
|
public function canResponseSatisfyFailedRequest(RequestInterface $request, Response $response) |
275
|
|
|
{ |
276
|
|
|
$reqc = $request->getHeader('Cache-Control'); |
277
|
|
|
$resc = $response->getHeader('Cache-Control'); |
278
|
|
|
$requestStaleIfError = $reqc ? $reqc->getDirective('stale-if-error') : null; |
279
|
|
|
$responseStaleIfError = $resc ? $resc->getDirective('stale-if-error') : null; |
280
|
|
|
|
281
|
|
|
if (!$requestStaleIfError && !$responseStaleIfError) { |
282
|
|
|
return false; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
if (is_numeric($requestStaleIfError) && $response->getAge() - $response->getMaxAge() > $requestStaleIfError) { |
286
|
|
|
return false; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
if (is_numeric($responseStaleIfError) && $response->getAge() - $response->getMaxAge() > $responseStaleIfError) { |
290
|
|
|
return false; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
return true; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Purge all cache entries for a given URL |
298
|
|
|
* |
299
|
|
|
* @param string $url URL to purge |
300
|
|
|
*/ |
301
|
|
|
public function purge($url) |
302
|
|
|
{ |
303
|
|
|
// BC compatibility with previous version that accepted a Request object |
304
|
|
|
$url = $url instanceof RequestInterface ? $url->getUrl() : $url; |
305
|
|
|
$this->storage->purge($url); |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Add the plugin's headers to a response |
310
|
|
|
* |
311
|
|
|
* @param RequestInterface $request Request |
312
|
|
|
* @param Response $response Response to add headers to |
313
|
|
|
*/ |
314
|
|
|
protected function addResponseHeaders(RequestInterface $request, Response $response) |
315
|
|
|
{ |
316
|
|
|
$params = $request->getParams(); |
317
|
|
|
$response->setHeader('Via', sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION)); |
318
|
|
|
|
319
|
|
|
$lookup = ($params['cache.lookup'] === true ? 'HIT' : 'MISS') . ' from GuzzleCache'; |
320
|
|
View Code Duplication |
if ($header = $response->getHeader('X-Cache-Lookup')) { |
|
|
|
|
321
|
|
|
// Don't add duplicates |
322
|
|
|
$values = $header->toArray(); |
323
|
|
|
$values[] = $lookup; |
324
|
|
|
$response->setHeader('X-Cache-Lookup', array_unique($values)); |
325
|
|
|
} else { |
326
|
|
|
$response->setHeader('X-Cache-Lookup', $lookup); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
if ($params['cache.hit'] === true) { |
330
|
|
|
$xcache = 'HIT from GuzzleCache'; |
331
|
|
|
} elseif ($params['cache.hit'] == 'error') { |
332
|
|
|
$xcache = 'HIT_ERROR from GuzzleCache'; |
333
|
|
|
} else { |
334
|
|
|
$xcache = 'MISS from GuzzleCache'; |
335
|
|
|
} |
336
|
|
|
|
337
|
|
View Code Duplication |
if ($header = $response->getHeader('X-Cache')) { |
|
|
|
|
338
|
|
|
// Don't add duplicates |
339
|
|
|
$values = $header->toArray(); |
340
|
|
|
$values[] = $xcache; |
341
|
|
|
$response->setHeader('X-Cache', array_unique($values)); |
342
|
|
|
} else { |
343
|
|
|
$response->setHeader('X-Cache', $xcache); |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
if ($response->isFresh() === false) { |
347
|
|
|
$response->addHeader('Warning', sprintf('110 GuzzleCache/%s "Response is stale"', Version::VERSION)); |
348
|
|
|
if ($params['cache.hit'] === 'error') { |
349
|
|
|
$response->addHeader('Warning', sprintf('111 GuzzleCache/%s "Revalidation failed"', Version::VERSION)); |
350
|
|
|
} |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
} |
354
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.