$options); } elseif ($options instanceof CacheStorageInterface) { $options = array('storage' => $options); } elseif (class_exists('Doctrine\Common\Cache\ArrayCache')) { $options = array('storage' => new DefaultCacheStorage(new DoctrineCacheAdapter(new ArrayCache()), 3600)); } else { // @codeCoverageIgnoreStart throw new InvalidArgumentException('No cache was provided and Doctrine is not installed'); // @codeCoverageIgnoreEnd } } // Add a cache storage if a cache adapter was provided if (!isset($options['adapter'])) { $this->storage = $options['storage']; } else { $this->storage = new DefaultCacheStorage( $options['adapter'], array_key_exists('default_ttl', $options) ? $options['default_ttl'] : 3600 ); } // Use the provided key provider or the default if (!isset($options['key_provider'])) { $this->keyProvider = new DefaultCacheKeyProvider(); } else { if (is_callable($options['key_provider'])) { $this->keyProvider = new CallbackCacheKeyProvider($options['key_provider']); } else { $this->keyProvider = $options['key_provider']; } } if (!isset($options['can_cache'])) { $this->canCache = new DefaultCanCacheStrategy(); } else { if (is_callable($options['can_cache'])) { $this->canCache = new CallbackCanCacheStrategy($options['can_cache']); } else { $this->canCache = $options['can_cache']; } } // Use the provided revalidation strategy or the default if (isset($options['revalidation'])) { $this->revalidation = $options['revalidation']; } else { $this->revalidation = new DefaultRevalidation($this->keyProvider, $this->storage, $this); } if (!isset($options['debug_headers'])) { $this->debugHeaders = true; } else { $this->debugHeaders = (bool) $options['debug_headers']; } } public static function getSubscribedEvents() { return array( 'request.before_send' => array('onRequestBeforeSend', -255), 'request.sent' => array('onRequestSent', 255), 'request.error' => array('onRequestError', 0), 'request.exception' => array('onRequestException', 0), ); } /** * Check if a response in cache will satisfy the request before sending * * @param Event $event */ public function onRequestBeforeSend(Event $event) { $request = $event['request']; $request->addHeader('Via', sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION)); // Intercept PURGE requests if ($request->getMethod() == 'PURGE') { $this->purge($request); $request->setResponse(new Response(200, array(), 'purged')); return; } if (!$this->canCache->canCacheRequest($request)) { return; } $hashKey = $this->keyProvider->getCacheKey($request); // If the cached data was found, then make the request into a // manually set request if ($cachedData = $this->storage->fetch($hashKey)) { $request->getParams()->set('cache.lookup', true); $response = new Response($cachedData[0], $cachedData[1], $cachedData[2]); $response->setHeader( 'Age', time() - strtotime($response->getDate() ? : $response->getLastModified() ?: 'now') ); // Validate that the response satisfies the request if ($this->canResponseSatisfyRequest($request, $response)) { $request->getParams()->set('cache.hit', true); $request->setResponse($response); } } } /** * If possible, store a response in cache after sending * * @param Event $event */ public function onRequestSent(Event $event) { $request = $event['request']; $response = $event['response']; $cacheKey = $this->keyProvider->getCacheKey($request); if ($request->getParams()->get('cache.hit') === null && $this->canCache->canCacheRequest($request) && $this->canCache->canCacheResponse($response) ) { $this->storage->cache($cacheKey, $response, $request->getParams()->get('cache.override_ttl')); } $this->addResponseHeaders($cacheKey, $request, $response); } /** * If possible, return a cache response on an error * * @param Event $event */ public function onRequestError(Event $event) { $request = $event['request']; if (!$this->canCache->canCacheRequest($request)) { return; } $cacheKey = $this->keyProvider->getCacheKey($request); if ($cachedData = $this->storage->fetch($cacheKey)) { $response = new Response($cachedData[0], $cachedData[1], $cachedData[2]); $response->setRequest($request); $response->setHeader( 'Age', time() - strtotime($response->getLastModified() ? : $response->getDate() ?: 'now') ); if ($this->canResponseSatisfyFailedRequest($request, $response)) { $request->getParams()->set('cache.hit', 'error'); $this->addResponseHeaders($cacheKey, $request, $response); $event['response'] = $response; $event->stopPropagation(); } } } /** * If possible, set a cache response on a cURL exception * * @param Event $event * * @return null */ public function onRequestException(Event $event) { if (!$event['exception'] instanceof CurlException) { return; } $request = $event['request']; if (!$this->canCache->canCacheRequest($request)) { return; } $cacheKey = $this->keyProvider->getCacheKey($request); if ($cachedData = $this->storage->fetch($cacheKey)) { $response = new Response($cachedData[0], $cachedData[1], $cachedData[2]); $response->setHeader('Age', time() - strtotime($response->getDate() ? : 'now')); if (!$this->canResponseSatisfyFailedRequest($request, $response)) { return; } $request->getParams()->set('cache.hit', 'error'); $request->setResponse($response); $event->stopPropagation(); } } /** * Check if a cache response satisfies a request's caching constraints * * @param RequestInterface $request Request to validate * @param Response $response Response to validate * * @return bool */ public function canResponseSatisfyRequest(RequestInterface $request, Response $response) { $responseAge = $response->calculateAge(); $reqc = $request->getHeader('Cache-Control'); $resc = $response->getHeader('Cache-Control'); // Check the request's max-age header against the age of the response if ($reqc && $reqc->hasDirective('max-age') && $responseAge > $reqc->getDirective('max-age')) { return false; } // Check the response's max-age header if ($response->isFresh() === false) { $maxStale = $reqc ? $reqc->getDirective('max-stale') : null; if (null !== $maxStale) { if ($maxStale !== true && $response->getFreshness() < (-1 * $maxStale)) { return false; } } elseif ($resc && $resc->hasDirective('max-age') && $responseAge > $resc->getDirective('max-age') ) { return false; } } // Only revalidate GET requests if ($request->getMethod() == RequestInterface::GET) { // Check if the response must be validated against the origin server if ($request->getHeader('Pragma') == 'no-cache' || ($reqc && ($reqc->hasDirective('no-cache') || $reqc->hasDirective('must-revalidate'))) || ($resc && ($resc->hasDirective('no-cache') || $resc->hasDirective('must-revalidate'))) ) { // no-cache: When no parameters are present, always revalidate // When parameters are present in no-cache and the request includes those same parameters, then the // response must re-validate. I'll need an example of what fields look like in order to implement a // smarter version of no-cache // Requests can decline to revalidate against the origin server by setting the cache.revalidate param: // - never - To never revalidate and always contact the origin server // - skip - To skip revalidation and just use what is in cache switch ($request->getParams()->get('cache.revalidate')) { case 'never': return false; case 'skip': return true; default: return $this->revalidation->revalidate($request, $response); } } } return true; } /** * Check if a cache response satisfies a failed request's caching constraints * * @param RequestInterface $request Request to validate * @param Response $response Response to validate * * @return bool */ public function canResponseSatisfyFailedRequest(RequestInterface $request, Response $response) { $reqc = $request->getHeader('Cache-Control'); $resc = $response->getHeader('Cache-Control'); $requestStaleIfError = $reqc ? $reqc->getDirective('stale-if-error') : null; $responseStaleIfError = $resc ? $resc->getDirective('stale-if-error') : null; if (!$requestStaleIfError && !$responseStaleIfError) { return false; } if (is_numeric($requestStaleIfError) && $response->getAge() - $response->getMaxAge() > $requestStaleIfError ) { return false; } if (is_numeric($responseStaleIfError) && $response->getAge() - $response->getMaxAge() > $responseStaleIfError ) { return false; } return true; } /** * Purge a request from the cache storage * * @param RequestInterface $request Request to purge */ public function purge(RequestInterface $request) { // If the request has a cache.purge_methods param, then use that, otherwise use the default known methods $methods = $request->getParams()->get('cache.purge_methods') ?: array('GET', 'HEAD', 'POST', 'PUT', 'DELETE'); foreach ($methods as $method) { // Clone the request with each method and clear from the cache $cloned = RequestFactory::getInstance()->cloneRequestWithMethod($request, $method); $key = $this->keyProvider->getCacheKey($cloned); $this->storage->delete($key); } } /** * Add the plugin's headers to a response * * @param string $cacheKey Cache key * @param RequestInterface $request Request * @param Response $response Response to add headers to */ protected function addResponseHeaders($cacheKey, RequestInterface $request, Response $response) { if (!$response->hasHeader('X-Guzzle-Cache')) { $response->setHeader('X-Guzzle-Cache', "key={$cacheKey}"); } $response->addHeader('Via', sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION)); if ($this->debugHeaders) { if ($request->getParams()->get('cache.lookup') === true) { $response->addHeader('X-Cache-Lookup', 'HIT from GuzzleCache'); } else { $response->addHeader('X-Cache-Lookup', 'MISS from GuzzleCache'); } if ($request->getParams()->get('cache.hit') === true) { $response->addHeader('X-Cache', 'HIT from GuzzleCache'); } elseif ($request->getParams()->get('cache.hit') === 'error') { $response->addHeader('X-Cache', 'HIT_ERROR from GuzzleCache'); } else { $response->addHeader('X-Cache', 'MISS from GuzzleCache'); } } if ($response->isFresh() === false) { $response->addHeader('Warning', sprintf('110 GuzzleCache/%s "Response is stale"', Version::VERSION)); if ($request->getParams()->get('cache.hit') === 'error') { $response->addHeader( 'Warning', sprintf('111 GuzzleCache/%s "Revalidation failed"', Version::VERSION) ); } } } }