mailgun-php/Mailgun/Guzzle/Plugin/Cache/CachePlugin.php

354 lines
13 KiB
PHP
Raw Normal View History

2013-07-18 09:48:12 +04:00
<?php
namespace Guzzle\Plugin\Cache;
use Guzzle\Cache\CacheAdapterFactory;
use Guzzle\Cache\CacheAdapterInterface;
use Guzzle\Common\Event;
use Guzzle\Common\Exception\InvalidArgumentException;
use Guzzle\Common\Version;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
use Guzzle\Cache\DoctrineCacheAdapter;
use Guzzle\Http\Exception\CurlException;
use Doctrine\Common\Cache\ArrayCache;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Plugin to enable the caching of GET and HEAD requests. Caching can be done on all requests passing through this
* plugin or only after retrieving resources with cacheable response headers.
*
* This is a simple implementation of RFC 2616 and should be considered a private transparent proxy cache, meaning
* authorization and private data can be cached.
*
* It also implements RFC 5861's `stale-if-error` Cache-Control extension, allowing stale cache responses to be used
* when an error is encountered (such as a `500 Internal Server Error` or DNS failure).
*/
class CachePlugin implements EventSubscriberInterface
{
/** @var RevalidationInterface Cache revalidation strategy */
protected $revalidation;
/** @var CanCacheStrategyInterface Object used to determine if a request can be cached */
protected $canCache;
/** @var CacheStorageInterface $cache Object used to cache responses */
protected $storage;
/** @var bool */
protected $autoPurge;
/**
* @param array|CacheAdapterInterface|CacheStorageInterface $options Array of options for the cache plugin,
* cache adapter, or cache storage object.
* - CacheStorageInterface storage: Adapter used to cache responses
* - RevalidationInterface revalidation: Cache revalidation strategy
* - CanCacheInterface can_cache: Object used to determine if a request can be cached
* - bool auto_purge Set to true to automatically PURGE resources when non-idempotent
* requests are sent to a resource. Defaults to false.
* @throws InvalidArgumentException if no cache is provided and Doctrine cache is not installed
*/
public function __construct($options = null)
{
if (!is_array($options)) {
if ($options instanceof CacheAdapterInterface) {
$options = array('storage' => new DefaultCacheStorage($options));
} elseif ($options instanceof CacheStorageInterface) {
$options = array('storage' => $options);
} elseif ($options) {
$options = array('storage' => new DefaultCacheStorage(CacheAdapterFactory::fromCache($options)));
} elseif (!class_exists('Doctrine\Common\Cache\ArrayCache')) {
// @codeCoverageIgnoreStart
throw new InvalidArgumentException('No cache was provided and Doctrine is not installed');
// @codeCoverageIgnoreEnd
}
}
$this->autoPurge = isset($options['auto_purge']) ? $options['auto_purge'] : false;
// Add a cache storage if a cache adapter was provided
$this->storage = isset($options['storage'])
? $options['storage']
: new DefaultCacheStorage(new DoctrineCacheAdapter(new ArrayCache()));
if (!isset($options['can_cache'])) {
$this->canCache = new DefaultCanCacheStrategy();
} else {
$this->canCache = is_callable($options['can_cache'])
? new CallbackCanCacheStrategy($options['can_cache'])
: $options['can_cache'];
}
// Use the provided revalidation strategy or the default
$this->revalidation = isset($options['revalidation'])
? $options['revalidation']
: new DefaultRevalidation($this->storage, $this->canCache);
}
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));
if (!$this->canCache->canCacheRequest($request)) {
switch ($request->getMethod()) {
case 'PURGE':
$this->purge($request);
$request->setResponse(new Response(200, array(), 'purged'));
break;
case 'PUT':
case 'POST':
case 'DELETE':
case 'PATCH':
if ($this->autoPurge) {
$this->purge($request);
}
}
return;
}
if ($response = $this->storage->fetch($request)) {
$params = $request->getParams();
$params['cache.lookup'] = true;
$response->setHeader(
'Age',
time() - strtotime($response->getDate() ? : $response->getLastModified() ?: 'now')
);
// Validate that the response satisfies the request
if ($this->canResponseSatisfyRequest($request, $response)) {
if (!isset($params['cache.hit'])) {
$params['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'];
if ($request->getParams()->get('cache.hit') === null &&
$this->canCache->canCacheRequest($request) &&
$this->canCache->canCacheResponse($response)
) {
$this->storage->cache($request, $response);
}
$this->addResponseHeaders($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;
}
if ($response = $this->storage->fetch($request)) {
$response->setHeader(
'Age',
time() - strtotime($response->getLastModified() ? : $response->getDate() ?: 'now')
);
if ($this->canResponseSatisfyFailedRequest($request, $response)) {
$request->getParams()->set('cache.hit', 'error');
$this->addResponseHeaders($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;
}
if ($response = $this->storage->fetch($request)) {
$response->setHeader('Age', time() - strtotime($response->getDate() ? : 'now'));
if (!$this->canResponseSatisfyFailedRequest($request, $response)) {
return;
}
$request->getParams()->set('cache.hit', 'error');
$request->setResponse($response);
$this->addResponseHeaders($request, $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;
}
}
if ($this->revalidation->shouldRevalidate($request, $response)) {
try {
return $this->revalidation->revalidate($request, $response);
} catch (CurlException $e) {
$request->getParams()->set('cache.hit', 'error');
return $this->canResponseSatisfyFailedRequest($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 all cache entries for a given URL
*
* @param string $url URL to purge
*/
public function purge($url)
{
// BC compatibility with previous version that accepted a Request object
$url = $url instanceof RequestInterface ? $url->getUrl() : $url;
$this->storage->purge($url);
}
/**
* Add the plugin's headers to a response
*
* @param RequestInterface $request Request
* @param Response $response Response to add headers to
*/
protected function addResponseHeaders(RequestInterface $request, Response $response)
{
$params = $request->getParams();
$response->setHeader('Via', sprintf('%s GuzzleCache/%s', $request->getProtocolVersion(), Version::VERSION));
$lookup = ($params['cache.lookup'] === true ? 'HIT' : 'MISS') . ' from GuzzleCache';
if ($header = $response->getHeader('X-Cache-Lookup')) {
// Don't add duplicates
$values = $header->toArray();
$values[] = $lookup;
$response->setHeader('X-Cache-Lookup', array_unique($values));
} else {
$response->setHeader('X-Cache-Lookup', $lookup);
}
if ($params['cache.hit'] === true) {
$xcache = 'HIT from GuzzleCache';
} elseif ($params['cache.hit'] == 'error') {
$xcache = 'HIT_ERROR from GuzzleCache';
} else {
$xcache = 'MISS from GuzzleCache';
}
if ($header = $response->getHeader('X-Cache')) {
// Don't add duplicates
$values = $header->toArray();
$values[] = $xcache;
$response->setHeader('X-Cache', array_unique($values));
} else {
$response->setHeader('X-Cache', $xcache);
}
if ($response->isFresh() === false) {
$response->addHeader('Warning', sprintf('110 GuzzleCache/%s "Response is stale"', Version::VERSION));
if ($params['cache.hit'] === 'error') {
$response->addHeader('Warning', sprintf('111 GuzzleCache/%s "Revalidation failed"', Version::VERSION));
}
}
}
}