1: <?php
2:
3: namespace Guzzle\Plugin\Cache;
4:
5: use Guzzle\Cache\CacheAdapterInterface;
6: use Guzzle\Common\Event;
7: use Guzzle\Common\Exception\InvalidArgumentException;
8: use Guzzle\Common\Version;
9: use Guzzle\Http\Message\RequestFactory;
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: 19: 20: 21: 22: 23: 24: 25: 26:
27: class CachePlugin implements EventSubscriberInterface
28: {
29: 30: 31:
32: protected $keyProvider;
33:
34: 35: 36:
37: protected $revalidation;
38:
39: 40: 41:
42: protected $canCache;
43:
44: 45: 46:
47: protected $storage;
48:
49: 50: 51:
52: protected ;
53:
54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70:
71: public function __construct($options = null)
72: {
73: if (!is_array($options)) {
74: if ($options instanceof CacheAdapterInterface) {
75: $options = array('adapter' => $options);
76: } elseif ($options instanceof CacheStorageInterface) {
77: $options = array('storage' => $options);
78: } elseif (class_exists('Doctrine\Common\Cache\ArrayCache')) {
79: $options = array('storage' => new DefaultCacheStorage(new DoctrineCacheAdapter(new ArrayCache()), 3600));
80: } else {
81:
82: throw new InvalidArgumentException('No cache was provided and Doctrine is not installed');
83:
84: }
85: }
86:
87:
88: if (!isset($options['adapter'])) {
89: $this->storage = $options['storage'];
90: } else {
91: $this->storage = new DefaultCacheStorage(
92: $options['adapter'],
93: array_key_exists('default_ttl', $options) ? $options['default_ttl'] : 3600
94: );
95: }
96:
97:
98: if (!isset($options['key_provider'])) {
99: $this->keyProvider = new DefaultCacheKeyProvider();
100: } else {
101: if (is_callable($options['key_provider'])) {
102: $this->keyProvider = new CallbackCacheKeyProvider($options['key_provider']);
103: } else {
104: $this->keyProvider = $options['key_provider'];
105: }
106: }
107:
108: if (!isset($options['can_cache'])) {
109: $this->canCache = new DefaultCanCacheStrategy();
110: } else {
111: if (is_callable($options['can_cache'])) {
112: $this->canCache = new CallbackCanCacheStrategy($options['can_cache']);
113: } else {
114: $this->canCache = $options['can_cache'];
115: }
116: }
117:
118:
119: if (isset($options['revalidation'])) {
120: $this->revalidation = $options['revalidation'];
121: } else {
122: $this->revalidation = new DefaultRevalidation($this->keyProvider, $this->storage, $this);
123: }
124:
125: if (!isset($options['debug_headers'])) {
126: $this->debugHeaders = true;
127: } else {
128: $this->debugHeaders = (bool) $options['debug_headers'];
129: }
130: }
131:
132: 133: 134:
135: public static function getSubscribedEvents()
136: {
137: return array(
138: 'request.before_send' => array('onRequestBeforeSend', -255),
139: 'request.sent' => array('onRequestSent', 255),
140: 'request.error' => array('onRequestError', 0),
141: 'request.exception' => array('onRequestException', 0),
142: );
143: }
144:
145: 146: 147: 148: 149:
150: public function onRequestBeforeSend(Event $event)
151: {
152: $request = $event['request'];
153: $request->addHeader('Via', sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION));
154:
155:
156: if ($request->getMethod() == 'PURGE') {
157: $this->purge($request);
158: $request->setResponse(new Response(200, array(), 'purged'));
159: return;
160: }
161:
162: if (!$this->canCache->canCacheRequest($request)) {
163: return;
164: }
165:
166: $hashKey = $this->keyProvider->getCacheKey($request);
167:
168:
169:
170: if ($cachedData = $this->storage->fetch($hashKey)) {
171: $request->getParams()->set('cache.lookup', true);
172: $response = new Response($cachedData[0], $cachedData[1], $cachedData[2]);
173: $response->setHeader(
174: 'Age',
175: time() - strtotime($response->getDate() ? : $response->getLastModified() ?: 'now')
176: );
177:
178: if ($this->canResponseSatisfyRequest($request, $response)) {
179: $request->getParams()->set('cache.hit', true);
180: $request->setResponse($response);
181: }
182: }
183: }
184:
185: 186: 187: 188: 189:
190: public function onRequestSent(Event $event)
191: {
192: $request = $event['request'];
193: $response = $event['response'];
194:
195: $cacheKey = $this->keyProvider->getCacheKey($request);
196:
197: if ($request->getParams()->get('cache.hit') === null &&
198: $this->canCache->canCacheRequest($request) &&
199: $this->canCache->canCacheResponse($response)
200: ) {
201: $this->storage->cache($cacheKey, $response, $request->getParams()->get('cache.override_ttl'));
202: }
203:
204: $this->addResponseHeaders($cacheKey, $request, $response);
205: }
206:
207: 208: 209: 210: 211:
212: public function onRequestError(Event $event)
213: {
214: $request = $event['request'];
215:
216: if (!$this->canCache->canCacheRequest($request)) {
217: return;
218: }
219:
220: $cacheKey = $this->keyProvider->getCacheKey($request);
221:
222: if ($cachedData = $this->storage->fetch($cacheKey)) {
223: $response = new Response($cachedData[0], $cachedData[1], $cachedData[2]);
224: $response->setRequest($request);
225: $response->setHeader(
226: 'Age',
227: time() - strtotime($response->getLastModified() ? : $response->getDate() ?: 'now')
228: );
229:
230: if ($this->canResponseSatisfyFailedRequest($request, $response)) {
231: $request->getParams()->set('cache.hit', 'error');
232: $this->addResponseHeaders($cacheKey, $request, $response);
233: $event['response'] = $response;
234: $event->stopPropagation();
235: }
236: }
237: }
238:
239: 240: 241: 242: 243: 244: 245:
246: public function onRequestException(Event $event)
247: {
248: if (!$event['exception'] instanceof CurlException) {
249: return;
250: }
251:
252: $request = $event['request'];
253: if (!$this->canCache->canCacheRequest($request)) {
254: return;
255: }
256:
257: $cacheKey = $this->keyProvider->getCacheKey($request);
258:
259: if ($cachedData = $this->storage->fetch($cacheKey)) {
260: $response = new Response($cachedData[0], $cachedData[1], $cachedData[2]);
261: $response->setHeader('Age', time() - strtotime($response->getDate() ? : 'now'));
262: if (!$this->canResponseSatisfyFailedRequest($request, $response)) {
263: return;
264: }
265: $request->getParams()->set('cache.hit', 'error');
266: $request->setResponse($response);
267: $event->stopPropagation();
268: }
269: }
270:
271: 272: 273: 274: 275: 276: 277: 278:
279: public function canResponseSatisfyRequest(RequestInterface $request, Response $response)
280: {
281: $responseAge = $response->getAge();
282:
283:
284: if ($request->hasCacheControlDirective('max-age') &&
285: $responseAge > $request->getCacheControlDirective('max-age')) {
286: return false;
287: }
288:
289:
290: if ($response->isFresh() === false) {
291: $maxStale = $request->getCacheControlDirective('max-stale');
292: if (null !== $maxStale) {
293: if ($maxStale !== true && $response->getFreshness() < (-1 * $maxStale)) {
294: return false;
295: }
296: } elseif ($response->hasCacheControlDirective('max-age')
297: && $responseAge > $response->getCacheControlDirective('max-age')
298: ) {
299: return false;
300: }
301: }
302:
303:
304: if ($request->getMethod() == RequestInterface::GET) {
305:
306: if ($request->getHeader('Pragma') == 'no-cache' ||
307: $request->hasCacheControlDirective('no-cache') ||
308: $request->hasCacheControlDirective('must-revalidate') ||
309: $response->hasCacheControlDirective('must-revalidate') ||
310: $response->hasCacheControlDirective('no-cache')) {
311:
312:
313:
314:
315:
316:
317:
318:
319: switch ($request->getParams()->get('cache.revalidate')) {
320: case 'never':
321: return false;
322: case 'skip':
323: return true;
324: default:
325: return $this->revalidation->revalidate($request, $response);
326: }
327: }
328: }
329:
330: return true;
331: }
332:
333: 334: 335: 336: 337: 338: 339: 340:
341: public function canResponseSatisfyFailedRequest(RequestInterface $request, Response $response)
342: {
343: $requestStaleIfError = $request->getCacheControlDirective('stale-if-error');
344: $responseStaleIfError = $response->getCacheControlDirective('stale-if-error');
345:
346: if (!$requestStaleIfError && !$responseStaleIfError) {
347: return false;
348: }
349:
350: if (is_numeric($requestStaleIfError) &&
351: $response->getAge() - $response->getMaxAge() > $requestStaleIfError
352: ) {
353: return false;
354: }
355:
356: if (is_numeric($responseStaleIfError) &&
357: $response->getAge() - $response->getMaxAge() > $responseStaleIfError
358: ) {
359: return false;
360: }
361:
362: return true;
363: }
364:
365: 366: 367: 368: 369:
370: public function purge(RequestInterface $request)
371: {
372:
373: $methods = $request->getParams()->get('cache.purge_methods') ?: array('GET', 'HEAD', 'POST', 'PUT', 'DELETE');
374: foreach ($methods as $method) {
375:
376: $cloned = RequestFactory::getInstance()->cloneRequestWithMethod($request, $method);
377: $key = $this->keyProvider->getCacheKey($cloned);
378: $this->storage->delete($key);
379: }
380: }
381:
382: 383: 384: 385: 386: 387: 388:
389: protected function ($cacheKey, RequestInterface $request, Response $response)
390: {
391: if (!$response->hasHeader('X-Guzzle-Cache')) {
392: $response->setHeader('X-Guzzle-Cache', "key={$cacheKey}");
393: }
394:
395: $response->addHeader('Via', sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION));
396:
397: if ($this->debugHeaders) {
398: if ($request->getParams()->get('cache.lookup') === true) {
399: $response->addHeader('X-Cache-Lookup', 'HIT from GuzzleCache');
400: } else {
401: $response->addHeader('X-Cache-Lookup', 'MISS from GuzzleCache');
402: }
403: if ($request->getParams()->get('cache.hit') === true) {
404: $response->addHeader('X-Cache', 'HIT from GuzzleCache');
405: } elseif ($request->getParams()->get('cache.hit') === 'error') {
406: $response->addHeader('X-Cache', 'HIT_ERROR from GuzzleCache');
407: } else {
408: $response->addHeader('X-Cache', 'MISS from GuzzleCache');
409: }
410: }
411:
412: if ($response->isFresh() === false) {
413: $response->addHeader('Warning', sprintf('110 GuzzleCache/%s "Response is stale"', Version::VERSION));
414: if ($request->getParams()->get('cache.hit') === 'error') {
415: $response->addHeader(
416: 'Warning',
417: sprintf('111 GuzzleCache/%s "Revalidation failed"', Version::VERSION)
418: );
419: }
420: }
421: }
422: }
423: