Overview

Namespaces

  • Contrib
    • Bundle
      • CoverallsBundle
        • Console
        • Entity
      • CoverallsV1Bundle
        • Api
        • Collector
        • Command
        • Config
        • Entity
          • Git
    • Component
      • File
      • Log
      • System
        • Git
  • Guzzle
    • Batch
      • Exception
    • Cache
    • Common
      • Exception
    • Http
      • Curl
      • Exception
      • Message
      • QueryAggregator
    • Inflection
    • Iterator
    • Log
    • Parser
      • Cookie
      • Message
      • UriTemplate
      • Url
    • Plugin
      • Async
      • Backoff
      • Cache
      • Cookie
        • CookieJar
        • Exception
      • CurlAuth
      • ErrorResponse
        • Exception
      • History
      • Log
      • Md5
      • Mock
      • Oauth
    • Service
      • Builder
      • Command
        • Factory
        • LocationVisitor
          • Request
          • Response
      • Description
      • Exception
      • Resource
    • Stream
  • PHP
  • Psr
    • Log
  • Symfony
    • Component
      • Config
        • Definition
          • Builder
          • Exception
        • Exception
        • Loader
        • Resource
        • Util
      • Console
        • Command
        • Formatter
        • Helper
        • Input
        • Output
        • Tester
      • EventDispatcher
        • Debug
      • Finder
        • Adapter
        • Comparator
        • Exception
        • Expression
        • Iterator
        • Shell
      • Stopwatch
      • Yaml
        • Exception

Classes

  • CachePlugin
  • CallbackCacheKeyProvider
  • CallbackCanCacheStrategy
  • DefaultCacheKeyProvider
  • DefaultCacheStorage
  • DefaultCanCacheStrategy
  • DefaultRevalidation
  • DenyRevalidation
  • SkipRevalidation

Interfaces

  • CacheKeyProviderInterface
  • CacheStorageInterface
  • CanCacheStrategyInterface
  • RevalidationInterface
  • Overview
  • Namespace
  • Class
  • Tree
  • Todo
  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:  * 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:     /**
 30:      * @var CacheKeyProviderInterface Cache key provider
 31:      */
 32:     protected $keyProvider;
 33: 
 34:     /**
 35:      * @var RevalidationInterface Cache revalidation strategy
 36:      */
 37:     protected $revalidation;
 38: 
 39:     /**
 40:      * @var CanCacheStrategyInterface Object used to determine if a request can be cached
 41:      */
 42:     protected $canCache;
 43: 
 44:     /**
 45:      * @var CacheStorageInterface $cache Object used to cache responses
 46:      */
 47:     protected $storage;
 48: 
 49:     /**
 50:      * @var bool Whether to add debug headers to the response
 51:      */
 52:     protected $debugHeaders;
 53: 
 54:     /**
 55:      * Construct a new CachePlugin. Cache options include the following:
 56:      *
 57:      * - CacheKeyProviderInterface key_provider:  (optional) Cache key provider
 58:      * - CacheAdapterInterface     adapter:       (optional) Adapter used to cache objects. Pass this or a cache_storage
 59:      * - CacheStorageInterface     storage:       (optional) Adapter used to cache responses
 60:      * - RevalidationInterface     revalidation:  (optional) Cache revalidation strategy
 61:      * - CanCacheInterface         can_cache:     (optional) Object used to determine if a request can be cached
 62:      * - int                       default_ttl:   (optional) Default TTL to use when caching if no cache_storage was set
 63:      *                                                       must set to 0 or it will assume the default of 3600 secs.
 64:      * - bool                      debug_headers: (optional) Add debug headers to the response (default true)
 65:      *
 66:      * @param array|CacheAdapterInterface|CacheStorageInterface $options Array of options for the cache plugin,
 67:      *                                                                   cache adapter, or cache storage object.
 68:      *
 69:      * @throws InvalidArgumentException if no cache is provided and Doctrine cache is not installed
 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:                 // @codeCoverageIgnoreStart
 82:                 throw new InvalidArgumentException('No cache was provided and Doctrine is not installed');
 83:                 // @codeCoverageIgnoreEnd
 84:             }
 85:         }
 86: 
 87:         // Add a cache storage if a cache adapter was provided
 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:         // Use the provided key provider or the default
 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:         // Use the provided revalidation strategy or the default
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:      * {@inheritdoc}
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:      * Check if a response in cache will satisfy the request before sending
147:      *
148:      * @param Event $event
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:         // Intercept PURGE requests
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:         // If the cached data was found, then make the request into a
169:         // manually set request
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:             // Validate that the response satisfies the request
178:             if ($this->canResponseSatisfyRequest($request, $response)) {
179:                 $request->getParams()->set('cache.hit', true);
180:                 $request->setResponse($response);
181:             }
182:         }
183:     }
184: 
185:     /**
186:      * If possible, store a response in cache after sending
187:      *
188:      * @param Event $event
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:      * If possible, return a cache response on an error
209:      *
210:      * @param Event $event
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:      * If possible, set a cache response on a cURL exception
241:      *
242:      * @param Event $event
243:      *
244:      * @return null
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:      * Check if a cache response satisfies a request's caching constraints
273:      *
274:      * @param RequestInterface $request  Request to validate
275:      * @param Response         $response Response to validate
276:      *
277:      * @return bool
278:      */
279:     public function canResponseSatisfyRequest(RequestInterface $request, Response $response)
280:     {
281:         $responseAge = $response->getAge();
282: 
283:         // Check the request's max-age header against the age of the response
284:         if ($request->hasCacheControlDirective('max-age') &&
285:             $responseAge > $request->getCacheControlDirective('max-age')) {
286:             return false;
287:         }
288: 
289:         // Check the response's max-age header
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:         // Only revalidate GET requests
304:         if ($request->getMethod() == RequestInterface::GET) {
305:             // Check if the response must be validated against the origin server
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:                 // no-cache: When no parameters are present, always revalidate
312:                 // When parameters are present in no-cache and the request includes those same parameters, then the
313:                 // response must re-validate. I'll need an example of what fields look like in order to implement a
314:                 // smarter version of no-cache
315: 
316:                 // Requests can decline to revalidate against the origin server by setting the cache.revalidate param:
317:                 // - never - To never revalidate and always contact the origin server
318:                 // - skip  - To skip revalidation and just use what is in cache
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:      * Check if a cache response satisfies a failed request's caching constraints
335:      *
336:      * @param RequestInterface $request  Request to validate
337:      * @param Response         $response Response to validate
338:      *
339:      * @return bool
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:      * Purge a request from the cache storage
367:      *
368:      * @param RequestInterface $request Request to purge
369:      */
370:     public function purge(RequestInterface $request)
371:     {
372:         // If the request has a cache.purge_methods param, then use that, otherwise use the default known methods
373:         $methods = $request->getParams()->get('cache.purge_methods') ?: array('GET', 'HEAD', 'POST', 'PUT', 'DELETE');
374:         foreach ($methods as $method) {
375:             // Clone the request with each method and clear from the cache
376:             $cloned = RequestFactory::getInstance()->cloneRequestWithMethod($request, $method);
377:             $key = $this->keyProvider->getCacheKey($cloned);
378:             $this->storage->delete($key);
379:         }
380:     }
381: 
382:     /**
383:      * Add the plugin's headers to a response
384:      *
385:      * @param string           $cacheKey Cache key
386:      * @param RequestInterface $request  Request
387:      * @param Response         $response Response to add headers to
388:      */
389:     protected function addResponseHeaders($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: 
php-coveralls API documentation generated by ApiGen 2.8.0