Download project

This commit is contained in:
Roman Pyrih
2024-11-20 09:09:44 +01:00
parent 547a138d6a
commit 5ff041757f
40737 changed files with 7766183 additions and 0 deletions

View File

@@ -0,0 +1,403 @@
<?php
namespace GuzzleHttp\Subscriber\Cache;
use Doctrine\Common\Cache\Cache;
use GuzzleHttp\Message\AbstractMessage;
use GuzzleHttp\Message\MessageInterface;
use GuzzleHttp\Message\Request;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\Stream;
use GuzzleHttp\Stream\StreamInterface;
/**
* Default cache storage implementation.
*/
class CacheStorage implements CacheStorageInterface
{
/** @var string */
private $keyPrefix;
/** @var int Default cache TTL */
private $defaultTtl;
/** @var Cache */
private $cache;
/** @var array Headers are excluded from the caching (see RFC 2616:13.5.1) */
private static $noCache = [
'age' => true,
'connection' => true,
'keep-alive' => true,
'proxy-authenticate' => true,
'proxy-authorization' => true,
'te' => true,
'trailers' => true,
'transfer-encoding' => true,
'upgrade' => true,
'set-cookie' => true,
'set-cookie2' => true,
];
/**
* @param Cache $cache Cache backend.
* @param string $keyPrefix (optional) Key prefix to add to each key.
* @param int $defaultTtl (optional) The default TTL to set, in seconds.
*/
public function __construct(Cache $cache, $keyPrefix = null, $defaultTtl = 0)
{
$this->cache = $cache;
$this->keyPrefix = $keyPrefix;
$this->defaultTtl = $defaultTtl;
}
public function cache(
RequestInterface $request,
ResponseInterface $response
) {
$ctime = time();
$ttl = $this->getTtl($response);
$key = $this->getCacheKey($request, $this->normalizeVary($response));
$headers = $this->persistHeaders($request);
$entries = $this->getManifestEntries($key, $ctime, $response, $headers);
$bodyDigest = null;
// Persist the Vary response header.
if ($response->hasHeader('vary')) {
$this->cacheVary($request, $response);
}
// Persist the response body if needed
if ($response->getBody() && $response->getBody()->getSize() > 0) {
$body = $response->getBody();
$bodyDigest = $this->getBodyKey($request->getUrl(), $body);
$this->cache->save($bodyDigest, (string) $body, $ttl);
}
array_unshift($entries, [
$headers,
$this->persistHeaders($response),
$response->getStatusCode(),
$bodyDigest,
$ctime + $ttl
]);
$this->cache->save($key, serialize($entries));
}
public function delete(RequestInterface $request)
{
$vary = $this->fetchVary($request);
$key = $this->getCacheKey($request, $vary);
$entries = $this->cache->fetch($key);
if (!$entries) {
return;
}
// Delete each cached body
foreach (unserialize($entries) as $entry) {
if ($entry[3]) {
$this->cache->delete($entry[3]);
}
}
// Delete any cached Vary header responses.
$this->deleteVary($request);
$this->cache->delete($key);
}
public function purge($url)
{
foreach (['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PURGE'] as $m) {
$this->delete(new Request($m, $url));
}
}
public function fetch(RequestInterface $request)
{
$vary = $this->fetchVary($request);
if ($vary) {
$key = $this->getCacheKey($request, $vary);
} else {
$key = $this->getCacheKey($request);
}
$entries = $this->cache->fetch($key);
if (!$entries) {
return null;
}
$match = $matchIndex = null;
$headers = $this->persistHeaders($request);
$entries = unserialize($entries);
foreach ($entries as $index => $entry) {
$vary = isset($entry[1]['vary']) ? $entry[1]['vary'] : '';
if ($this->requestsMatch($vary, $headers, $entry[0])) {
$match = $entry;
$matchIndex = $index;
break;
}
}
if (!$match) {
return null;
}
// Ensure that the response is not expired
$response = null;
if ($match[4] < time()) {
$response = -1;
} else {
$response = new Response($match[2], $match[1]);
if ($match[3]) {
if ($body = $this->cache->fetch($match[3])) {
$response->setBody(Stream\Utils::create($body));
} else {
// The response is not valid because the body was somehow
// deleted
$response = -1;
}
}
}
if ($response === -1) {
// Remove the entry from the metadata and update the cache
unset($entries[$matchIndex]);
if ($entries) {
$this->cache->save($key, serialize($entries));
} else {
$this->cache->delete($key);
}
return null;
}
return $response;
}
/**
* Hash a request URL into a string that returns cache metadata.
*
* @param RequestInterface $request The Request to generate the cache key
* for.
* @param array $vary (optional) An array of headers to vary
* the cache key by.
*
* @return string
*/
private function getCacheKey(RequestInterface $request, array $vary = [])
{
$key = $request->getMethod() . ' ' . $request->getUrl();
// If Vary headers have been passed in, fetch each header and add it to
// the cache key.
foreach ($vary as $header) {
$key .= " $header: " . $request->getHeader($header);
}
return $this->keyPrefix . md5($key);
}
/**
* Create a cache key for a response's body.
*
* @param string $url URL of the entry
* @param StreamInterface $body Response body
*
* @return string
*/
private function getBodyKey($url, StreamInterface $body)
{
return $this->keyPrefix . md5($url) . Stream\Utils::hash($body, 'md5');
}
/**
* Determines whether two Request HTTP header sets are non-varying.
*
* @param string $vary Response vary header
* @param array $r1 HTTP header array
* @param array $r2 HTTP header array
*
* @return bool
*/
private function requestsMatch($vary, $r1, $r2)
{
if ($vary) {
foreach (explode(',', $vary) as $header) {
$key = trim(strtolower($header));
$v1 = isset($r1[$key]) ? $r1[$key] : null;
$v2 = isset($r2[$key]) ? $r2[$key] : null;
if ($v1 !== $v2) {
return false;
}
}
}
return true;
}
/**
* Creates an array of cacheable and normalized message headers.
*
* @param MessageInterface $message
*
* @return array
*/
private function persistHeaders(MessageInterface $message)
{
// Clone the response to not destroy any necessary headers when caching
$headers = array_diff_key($message->getHeaders(), self::$noCache);
// Cast the headers to a string
foreach ($headers as &$value) {
$value = implode(', ', $value);
}
return $headers;
}
/**
* Return the TTL to use when caching a Response.
*
* @param ResponseInterface $response The response being cached.
*
* @return int The TTL in seconds.
*/
private function getTtl(ResponseInterface $response)
{
$ttl = 0;
if ($cacheControl = $response->getHeader('Cache-Control')) {
$maxAge = Utils::getDirective($response, 'max-age');
if (is_numeric($maxAge)) {
$ttl += $maxAge;
}
// According to RFC5861 stale headers are *in addition* to any
// max-age values.
$stale = Utils::getDirective($response, 'stale-if-error');
if (is_numeric($stale)) {
$ttl += $stale;
}
} elseif ($expires = $response->getHeader('Expires')) {
$ttl += strtotime($expires) - time();
}
return $ttl ?: $this->defaultTtl;
}
private function getManifestEntries(
$key,
$currentTime,
ResponseInterface $response,
$persistedRequest
) {
$entries = [];
$manifest = $this->cache->fetch($key);
if (!$manifest) {
return $entries;
}
// Determine which cache entries should still be in the cache
$vary = $response->getHeader('Vary');
foreach (unserialize($manifest) as $entry) {
// Check if the entry is expired
if ($entry[4] < $currentTime) {
continue;
}
$varyCmp = isset($entry[1]['vary']) ? $entries[1]['vary'] : '';
if ($vary != $varyCmp ||
!$this->requestsMatch($vary, $entry[0], $persistedRequest)
) {
$entries[] = $entry;
}
}
return $entries;
}
/**
* Return a sorted list of Vary headers.
*
* While headers are case-insensitive, header values are not. We can only
* normalize the order of headers to combine cache entries.
*
* @param ResponseInterface $response The Response with Vary headers.
*
* @return array An array of sorted headers.
*/
private function normalizeVary(ResponseInterface $response)
{
$parts = AbstractMessage::normalizeHeader($response, 'vary');
sort($parts);
return $parts;
}
/**
* Cache the Vary headers from a response.
*
* @param RequestInterface $request The Request that generated the Vary
* headers.
* @param ResponseInterface $response The Response with Vary headers.
*/
private function cacheVary(
RequestInterface $request,
ResponseInterface $response
) {
$key = $this->getVaryKey($request);
$this->cache->save($key, $this->normalizeVary($response), $this->getTtl($response));
}
/**
* Fetch the Vary headers associated with a request, if they exist.
*
* Only responses, and not requests, contain Vary headers. However, we need
* to be able to determine what Vary headers were set for a given URL and
* request method on a future request.
*
* @param RequestInterface $request The Request to fetch headers for.
*
* @return array An array of headers.
*/
private function fetchVary(RequestInterface $request)
{
$key = $this->getVaryKey($request);
$varyHeaders = $this->cache->fetch($key);
return is_array($varyHeaders) ? $varyHeaders : [];
}
/**
* Delete the headers associated with a Vary request.
*
* @param RequestInterface $request The Request to delete headers for.
*/
private function deleteVary(RequestInterface $request)
{
$key = $this->getVaryKey($request);
$this->cache->delete($key);
}
/**
* Get the cache key for Vary headers.
*
* @param RequestInterface $request The Request to fetch the key for.
*
* @return string The generated key.
*/
private function getVaryKey(RequestInterface $request)
{
$key = $this->keyPrefix . md5('vary ' . $this->getCacheKey($request));
return $key;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace GuzzleHttp\Subscriber\Cache;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
/**
* Interface used to cache HTTP responses.
*/
interface CacheStorageInterface
{
/**
* Get a Response from the cache for a request.
*
* @param RequestInterface $request
*
* @return null|ResponseInterface
*/
public function fetch(RequestInterface $request);
/**
* Cache an HTTP request.
*
* @param RequestInterface $request Request being cached
* @param ResponseInterface $response Response to cache
*/
public function cache(
RequestInterface $request,
ResponseInterface $response
);
/**
* Deletes cache entries that match a request.
*
* @param RequestInterface $request Request to delete from cache
*/
public function delete(RequestInterface $request);
/**
* Purge all cache entries for a given URL.
*
* @param string $url
*/
public function purge($url);
}

View File

@@ -0,0 +1,275 @@
<?php
namespace GuzzleHttp\Subscriber\Cache;
use Doctrine\Common\Cache\ArrayCache;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Event\BeforeEvent;
use GuzzleHttp\Event\CompleteEvent;
use GuzzleHttp\Event\ErrorEvent;
use GuzzleHttp\Event\HasEmitterInterface;
use GuzzleHttp\Event\RequestEvents;
use GuzzleHttp\Event\SubscriberInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
/**
* 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 CacheSubscriber implements SubscriberInterface
{
/** @var CacheStorageInterface $cache Object used to cache responses */
private $storage;
/** @var callable Determines if a request is cacheable */
private $canCache;
/**
* @param CacheStorageInterface $cache Cache storage
* @param callable $canCache Callable used to determine if a
* request can be cached. Accepts a
* RequestInterface and returns a
* boolean value.
*/
public function __construct(
CacheStorageInterface $cache,
callable $canCache
) {
$this->storage = $cache;
$this->canCache = $canCache;
}
/**
* Helper method used to easily attach a cache to a request or client.
*
* This method accepts an array of options that are used to control the
* caching behavior:
*
* - storage: An optional GuzzleHttp\Subscriber\Cache\CacheStorageInterface.
* If no value is not provided, an in-memory array cache will be used.
* - validate: Boolean value that determines if cached response are ever
* validated against the origin server. Defaults to true but can be
* disabled by passing false.
* - purge: Boolean value that determines if cached responses are purged
* when non-idempotent requests are sent to their URI. Defaults to true
* but can be disabled by passing false.
* - can_cache: An optional callable used to determine if a request can be
* cached. The callable accepts a RequestInterface and returns a boolean
* value. If no value is provided, the default behavior is utilized.
*
* @param HasEmitterInterface $subject Client or request to attach to,
* @param array $options Options used to control the cache.
*
* @return array Returns an associative array containing a 'subscriber' key
* that holds the created CacheSubscriber, and a 'storage'
* key that contains the cache storage used by the subscriber.
*/
public static function attach(
HasEmitterInterface $subject,
array $options = []
) {
if (!isset($options['storage'])) {
$options['storage'] = new CacheStorage(new ArrayCache());
}
if (!isset($options['can_cache'])) {
$options['can_cache'] = [
'GuzzleHttp\Subscriber\Cache\Utils',
'canCacheRequest',
];
}
$emitter = $subject->getEmitter();
$cache = new self($options['storage'], $options['can_cache']);
$emitter->attach($cache);
if (!isset($options['validate']) || $options['validate'] === true) {
$emitter->attach(new ValidationSubscriber(
$options['storage'],
$options['can_cache'])
);
}
if (!isset($options['purge']) || $options['purge'] === true) {
$emitter->attach(new PurgeSubscriber($options['storage']));
}
return ['subscriber' => $cache, 'storage' => $options['storage']];
}
public function getEvents()
{
return [
'before' => ['onBefore', RequestEvents::LATE],
'complete' => ['onComplete', RequestEvents::EARLY],
'error' => ['onError', RequestEvents::EARLY]
];
}
/**
* Checks if a request can be cached, and if so, intercepts with a cached
* response is available.
*
* @param BeforeEvent $event
*/
public function onBefore(BeforeEvent $event)
{
$request = $event->getRequest();
if (!$this->canCacheRequest($request)) {
$this->cacheMiss($request);
return;
}
if (!($response = $this->storage->fetch($request))) {
$this->cacheMiss($request);
return;
}
$response->setHeader('Age', Utils::getResponseAge($response));
$valid = $this->validate($request, $response);
// Validate that the response satisfies the request
if ($valid) {
$request->getConfig()->set('cache_lookup', 'HIT');
$request->getConfig()->set('cache_hit', true);
$event->intercept($response);
} else {
$this->cacheMiss($request);
}
}
/**
* Checks if the request and response can be cached, and if so, store it.
*
* @param CompleteEvent $event
*/
public function onComplete(CompleteEvent $event)
{
$request = $event->getRequest();
$response = $event->getResponse();
// Cache the response if it can be cached and isn't already
if ($request->getConfig()->get('cache_lookup') === 'MISS'
&& call_user_func($this->canCache, $request)
&& Utils::canCacheResponse($response)
) {
// Store the date when the response was cached
$response->setHeader('X-Guzzle-Cache-Date', gmdate('D, d M Y H:i:s T', time()));
$this->storage->cache($request, $response);
}
$this->addResponseHeaders($request, $response);
}
/**
* If the request failed, then check if a cached response would suffice.
*
* @param ErrorEvent $event
*/
public function onError(ErrorEvent $event)
{
$request = $event->getRequest();
if (!call_user_func($this->canCache, $request)) {
return;
}
$response = $this->storage->fetch($request);
// Intercept the failed response if possible
if ($response && $this->validateFailed($request, $response)) {
$request->getConfig()->set('cache_hit', 'error');
$response->setHeader('Age', Utils::getResponseAge($response));
$event->intercept($response);
}
}
private function cacheMiss(RequestInterface $request)
{
$request->getConfig()->set('cache_lookup', 'MISS');
}
private function validate(
RequestInterface $request,
ResponseInterface $response
) {
// Validation is handled in another subscriber and can be optionally
// enabled/disabled.
if (Utils::getDirective($response, 'must-revalidate')) {
return true;
}
return Utils::isResponseValid($request, $response);
}
private function validateFailed(
RequestInterface $request,
ResponseInterface $response
) {
$req = Utils::getDirective($request, 'stale-if-error');
$res = Utils::getDirective($response, 'stale-if-error');
if (!$req && !$res) {
return false;
}
$responseAge = Utils::getResponseAge($response);
$maxAge = Utils::getMaxAge($response);
if (($req && $responseAge - $maxAge > $req) ||
($responseAge - $maxAge > $res)
) {
return false;
}
return true;
}
private function canCacheRequest(RequestInterface $request)
{
return !$request->getConfig()->get('cache.disable')
&& call_user_func($this->canCache, $request);
}
private function addResponseHeaders(
RequestInterface $request,
ResponseInterface $response
) {
$params = $request->getConfig();
$lookup = $params['cache_lookup'] . ' from GuzzleCache';
$response->addHeader('X-Cache-Lookup', $lookup);
if ($params['cache_hit'] === true) {
$response->addHeader('X-Cache', 'HIT from GuzzleCache');
} elseif ($params['cache_hit'] == 'error') {
$response->addHeader('X-Cache', 'HIT_ERROR from GuzzleCache');
} else {
$response->addHeader('X-Cache', 'MISS from GuzzleCache');
}
$freshness = Utils::getFreshness($response);
// Only add a Warning header if we are returning a stale response.
if ($params['cache_hit'] && $freshness !== null && $freshness <= 0) {
$response->addHeader(
'Warning',
sprintf(
'%d GuzzleCache/' . ClientInterface::VERSION . ' "%s"',
110,
'Response is stale'
)
);
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace GuzzleHttp\Subscriber\Cache;
use GuzzleHttp\Event\BeforeEvent;
use GuzzleHttp\Event\RequestEvents;
use GuzzleHttp\Event\SubscriberInterface;
use GuzzleHttp\Message\Response;
/**
* Automatically purges a URL when a non-idempotent request is made to it.
*/
class PurgeSubscriber implements SubscriberInterface
{
/** @var CacheStorageInterface */
private $storage;
/** @var array */
private static $purgeMethods = [
'PUT' => true,
'POST' => true,
'DELETE' => true,
'PATCH' => true,
'PURGE' => true,
];
/**
* @param CacheStorageInterface $storage Storage to modify if purging
*/
public function __construct($storage)
{
$this->storage = $storage;
}
public function getEvents()
{
return ['before' => ['onBefore', RequestEvents::LATE]];
}
public function onBefore(BeforeEvent $event)
{
$request = $event->getRequest();
if (isset(self::$purgeMethods[$request->getMethod()])) {
$this->storage->purge($request->getUrl());
if ('PURGE' === $request->getMethod()) {
$event->intercept(new Response(204));
}
}
}
}

View File

@@ -0,0 +1,202 @@
<?php
namespace GuzzleHttp\Subscriber\Cache;
use GuzzleHttp\Message\AbstractMessage;
use GuzzleHttp\Message\MessageInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
/**
* Cache utility functions.
*/
class Utils
{
/**
* Get a cache control directive from a message.
*
* @param MessageInterface $message Message to retrieve
* @param string $part Cache directive to retrieve
*
* @return mixed|bool|null
*/
public static function getDirective(MessageInterface $message, $part)
{
$parts = AbstractMessage::parseHeader($message, 'Cache-Control');
foreach ($parts as $line) {
if (isset($line[$part])) {
return $line[$part];
} elseif (in_array($part, $line)) {
return true;
}
}
return null;
}
/**
* Gets the age of a response in seconds.
*
* @param ResponseInterface $response
*
* @return int
*/
public static function getResponseAge(ResponseInterface $response)
{
if ($response->hasHeader('Age')) {
return (int) $response->getHeader('Age');
}
$date = strtotime($response->getHeader('Date') ?: 'now');
return time() - $date;
}
/**
* Gets the number of seconds from the current time in which a response
* is still considered fresh.
*
* @param ResponseInterface $response
*
* @return int|null Returns the number of seconds
*/
public static function getMaxAge(ResponseInterface $response)
{
$smaxage = Utils::getDirective($response, 's-maxage');
if (is_numeric($smaxage)) {
return (int) $smaxage;
}
$maxage = Utils::getDirective($response, 'max-age');
if (is_numeric($maxage)) {
return (int) $maxage;
}
if ($response->hasHeader('Expires')) {
return strtotime($response->getHeader('Expires')) - time();
}
return null;
}
/**
* Get the freshness of a response by returning the difference of the
* maximum lifetime of the response and the age of the response.
*
* Freshness values less than 0 mean that the response is no longer fresh
* and is ABS(freshness) seconds expired. Freshness values of greater than
* zero is the number of seconds until the response is no longer fresh.
* A NULL result means that no freshness information is available.
*
* @param ResponseInterface $response Response to get freshness of
*
* @return int|null
*/
public static function getFreshness(ResponseInterface $response)
{
$maxAge = self::getMaxAge($response);
$age = self::getResponseAge($response);
return is_int($maxAge) && is_int($age) ? ($maxAge - $age) : null;
}
/**
* Default function used to determine if a request can be cached.
*
* @param RequestInterface $request Request to check
*
* @return bool
*/
public static function canCacheRequest(RequestInterface $request)
{
$method = $request->getMethod();
// Only GET and HEAD requests can be cached
if ($method !== 'GET' && $method !== 'HEAD') {
return false;
}
// Don't fool with Range requests for now
if ($request->hasHeader('Range')) {
return false;
}
return self::getDirective($request, 'no-store') === null;
}
/**
* Determines if a response can be cached.
*
* @param ResponseInterface $response Response to check
*
* @return bool
*/
public static function canCacheResponse(ResponseInterface $response)
{
static $cacheCodes = [200, 203, 300, 301, 410];
// Check if the response is cacheable based on the code
if (!in_array((int) $response->getStatusCode(), $cacheCodes)) {
return false;
}
// Make sure a valid body was returned and can be cached
$body = $response->getBody();
if ($body && (!$body->isReadable() || !$body->isSeekable())) {
return false;
}
// Never cache no-store resources (this is a private cache, so private
// can be cached)
if (self::getDirective($response, 'no-store')) {
return false;
}
// Don't fool with Content-Range requests for now
if ($response->hasHeader('Content-Range')) {
return false;
}
$freshness = self::getFreshness($response);
return $freshness === null // No freshness info.
|| $freshness >= 0 // It's fresh
|| $response->hasHeader('ETag') // Can validate
|| $response->hasHeader('Last-Modified'); // Can validate
}
public static function isResponseValid(
RequestInterface $request,
ResponseInterface $response
) {
$responseAge = Utils::getResponseAge($response);
$maxAge = Utils::getDirective($response, 'max-age');
// Increment the age based on the X-Guzzle-Cache-Date
if ($cacheDate = $response->getHeader('X-Guzzle-Cache-Date')) {
$responseAge += (time() - strtotime($cacheDate));
$response->setHeader('Age', $responseAge);
}
// Check the request's max-age header against the age of the response
if ($maxAge !== null && $responseAge > $maxAge) {
return false;
}
// Check the response's max-age header against the freshness level
$freshness = Utils::getFreshness($response);
if ($freshness !== null) {
$maxStale = Utils::getDirective($request, 'max-stale');
if ($maxStale !== null) {
if ($freshness < (-1 * $maxStale)) {
return false;
}
} elseif ($maxAge !== null && $responseAge > $maxAge) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,207 @@
<?php
namespace GuzzleHttp\Subscriber\Cache;
use GuzzleHttp\Event\CompleteEvent;
use GuzzleHttp\Event\RequestEvents;
use GuzzleHttp\Event\SubscriberInterface;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
/**
* Validates cached responses as needed.
*
* @Link http://tools.ietf.org/html/rfc7234#section-4.3
*/
class ValidationSubscriber implements SubscriberInterface
{
/** @var CacheStorageInterface Cache object storing cache data */
private $storage;
/** @var callable */
private $canCache;
/** @var array */
private static $gone = [404 => true, 410 => true];
/** @var array */
private static $replaceHeaders = [
'Date',
'Expires',
'Cache-Control',
'ETag',
'Last-Modified',
];
/**
* @param CacheStorageInterface $cache Cache storage
* @param callable $canCache Callable used to determine if a
* request can be cached. Accepts a
* RequestInterface and returns a
* boolean value.
*/
public function __construct(
CacheStorageInterface $cache,
callable $canCache
) {
$this->storage = $cache;
$this->canCache = $canCache;
}
public function getEvents()
{
return ['complete' => ['onComplete', RequestEvents::EARLY]];
}
public function onComplete(CompleteEvent $e)
{
$lookup = $e->getRequest()->getConfig()->get('cache_lookup');
if ($lookup == 'HIT' &&
$this->shouldvalidate($e->getRequest(), $e->getResponse())
) {
$this->validate($e->getRequest(), $e->getResponse(), $e);
}
}
private function validate(
RequestInterface $request,
ResponseInterface $response,
CompleteEvent $event
) {
try {
$validate = $this->createRevalidationRequest($request, $response);
$validated = $event->getClient()->send($validate);
} catch (BadResponseException $e) {
$this->handleBadResponse($e);
}
if ($validated->getStatusCode() == 200) {
$this->handle200Response($request, $validated, $event);
} elseif ($validated->getStatusCode() == 304) {
$this->handle304Response($request, $response, $validated, $event);
}
}
private function shouldValidate(
RequestInterface $request,
ResponseInterface $response
) {
if ($request->getMethod() != 'GET'
|| $request->getConfig()->get('cache.disable')
) {
return false;
}
$validate = Utils::getDirective($request, 'Pragma') === 'no-cache'
|| Utils::getDirective($response, 'Pragma') === 'no-cache'
|| Utils::getDirective($request, 'must-revalidate')
|| Utils::getDirective($response, 'must-revalidate')
|| Utils::getDirective($request, 'no-cache')
|| Utils::getDirective($response, 'no-cache')
|| Utils::getDirective($response, 'max-age') === '0'
|| Utils::getDirective($response, 's-maxage') === '0';
// Use the strong ETag validator if available and the response contains
// no Cache-Control directive
if (!$validate
&& !$response->hasHeader('Cache-Control')
&& $response->hasHeader('ETag')
) {
$validate = true;
}
return $validate;
}
/**
* Handles a bad response when attempting to validate.
*
* If the resource no longer exists, then remove from the cache.
*
* @param BadResponseException $e Exception encountered
*
* @throws BadResponseException
*/
private function handleBadResponse(BadResponseException $e)
{
if (isset(self::$gone[$e->getResponse()->getStatusCode()])) {
$this->storage->delete($e->getRequest());
}
throw $e;
}
/**
* Creates a request to use for revalidation.
*
* @param RequestInterface $request Request
* @param ResponseInterface $response Response to validate
*
* @return RequestInterface returns a revalidation request
*/
private function createRevalidationRequest(
RequestInterface $request,
ResponseInterface $response
) {
$validate = clone $request;
$validate->getConfig()->set('cache.disable', true);
$validate->removeHeader('Pragma');
$validate->removeHeader('Cache-Control');
$responseDate = $response->getHeader('Last-Modified')
?: $response->getHeader('Date');
$validate->setHeader('If-Modified-Since', $responseDate);
if ($etag = $response->getHeader('ETag')) {
$validate->setHeader('If-None-Match', $etag);
}
return $validate;
}
private function handle200Response(
RequestInterface $request,
ResponseInterface $validateResponse,
CompleteEvent $event
) {
// Store the 200 response in the cache if possible
if (Utils::canCacheResponse($validateResponse)) {
$this->storage->cache($request, $validateResponse);
}
$event->intercept($validateResponse);
}
private function handle304Response(
RequestInterface $request,
ResponseInterface $response,
ResponseInterface $validated,
CompleteEvent $event
) {
// Make sure that this response has the same ETag
if ($validated->getHeader('ETag') !== $response->getHeader('ETag')) {
// Revalidation failed, so remove from cache and retry.
$this->storage->delete($request);
$event->intercept($event->getClient()->send($request));
return;
}
// Replace cached headers with any of these headers from the
// origin server that might be more up to date
$modified = false;
foreach (self::$replaceHeaders as $name) {
if ($validated->hasHeader($name)
&& $validated->getHeader($name) != $response->getHeader($name)
) {
$modified = true;
$response->setHeader($name, $validated->getHeader($name));
}
}
// Store the updated response in cache
if ($modified) {
$this->storage->cache($request, $response);
}
}
}