/** * @param ResponseInterface $response * @return CacheEntry|null entry to save, null if can't cache it */ protected function getCacheObject(ResponseInterface $response) { if ($response->hasHeader("Cache-Control")) { $values = new KeyValueHttpHeader($response->getHeader("Cache-Control")); if ($values->has('no-store')) { // No store allowed (maybe some sensitives data...) return null; } if ($values->has('no-cache')) { // Stale response see RFC7234 section 5.2.1.4 $entry = new CacheEntry($response, new \DateTime('-1 seconds')); return $entry->hasValidationInformation() ? $entry : null; } if ($values->has('max-age')) { return new CacheEntry($response, new \DateTime('+' . $values->get('max-age') . 'seconds')); } return new CacheEntry($response, new \DateTime()); } if ($response->hasHeader("Expires")) { $expireAt = \DateTime::createFromFormat('D, d M Y H:i:s T', $response->getHeaderLine("Expires")); if ($expireAt !== FALSE) { return new CacheEntry($response, $expireAt); } } return new CacheEntry($response, new \DateTime('-1 seconds')); }
/** * @param ResponseInterface $response * @param \DateTime $staleAt * @param \DateTime|null $staleIfErrorTo if null, detected with the headers (RFC 5861) * @param \DateTime $staleWhileRevalidateTo */ public function __construct(ResponseInterface $response, \DateTime $staleAt, \DateTime $staleIfErrorTo = null, \DateTime $staleWhileRevalidateTo = null) { $this->response = $response; $this->staleAt = $staleAt; $values = new KeyValueHttpHeader($response->getHeader("Cache-Control")); if ($staleIfErrorTo === null && $values->has('stale-if-error')) { $this->staleIfErrorTo = (new \DateTime())->setTimestamp($this->staleAt->getTimestamp() + (int) $values->get('stale-if-error')); } else { $this->staleIfErrorTo = $staleIfErrorTo; } if ($staleWhileRevalidateTo === null && $values->has('stale-while-revalidate')) { $this->staleWhileRevalidateTo = new \DateTime('+' . $values->get('stale-while-revalidate') . 'seconds'); } else { $this->staleWhileRevalidateTo = $staleWhileRevalidateTo; } }
public function testBase() { $response = new Response(200, ['Cache-Control' => ['max-age = 120', ' stale-while-revalidate= 60 ', ' private ', 'zero=0', 'nothing = ', 'false = false']]); $values = new KeyValueHttpHeader($response->getHeader('Cache-Control')); $this->assertTrue($values->has('max-age')); $this->assertTrue($values->has('stale-while-revalidate')); $this->assertTrue($values->has('private')); $this->assertTrue($values->has('zero')); $this->assertTrue($values->has('nothing')); $this->assertTrue($values->has('false')); $this->assertEquals(120, $values->get('max-age')); $this->assertEquals(60, $values->get('stale-while-revalidate')); $this->assertEquals(0, $values->get('zero')); $this->assertEquals('', $values->get('nothing')); $this->assertEquals('false', $values->get('false')); }
/** * @param RequestInterface $request * @param ResponseInterface $response * @param \DateTime $staleAt * @param \DateTime|null $staleIfErrorTo if null, detected with the headers (RFC 5861) * @param \DateTime|null $staleWhileRevalidateTo */ public function __construct(RequestInterface $request, ResponseInterface $response, \DateTime $staleAt, \DateTime $staleIfErrorTo = null, \DateTime $staleWhileRevalidateTo = null) { $this->dateCreated = new \DateTime(); $this->request = $request; $this->response = $response; $this->staleAt = $staleAt; $values = new KeyValueHttpHeader($response->getHeader('Cache-Control')); if ($staleIfErrorTo === null && $values->has('stale-if-error')) { $this->staleIfErrorTo = new \DateTime('@' . ($this->staleAt->getTimestamp() + (int) $values->get('stale-if-error'))); } else { $this->staleIfErrorTo = $staleIfErrorTo; } if ($staleWhileRevalidateTo === null && $values->has('stale-while-revalidate')) { $this->staleWhileRevalidateTo = new \DateTime('@' . ($this->staleAt->getTimestamp() + (int) $values->get('stale-while-revalidate'))); } else { $this->staleWhileRevalidateTo = $staleWhileRevalidateTo; } }
/** * @param ResponseInterface $response * @param KeyValueHttpHeader $cacheControl * @return CacheEntry|null */ protected function getCacheObjectForCacheControl(ResponseInterface $response, KeyValueHttpHeader $cacheControl) { if ($cacheControl->has('no-store')) { // No store allowed (maybe some sensitives data...) return null; } if ($cacheControl->has('no-cache')) { // Stale response see RFC7234 section 5.2.1.4 $entry = new CacheEntry($response, new \DateTime('-1 seconds')); return $entry->hasValidationInformation() ? $entry : null; } if ($cacheControl->has('max-age')) { return new CacheEntry($response, new \DateTime('+' . (int) $cacheControl->get('max-age') . 'seconds')); } return new CacheEntry($response, new \DateTime()); }
/** * Return a CacheEntry or null if no cache. * * @param RequestInterface $request * * @return CacheEntry|null */ public function fetch(RequestInterface $request) { /** @var int|null $maxAge */ $maxAge = null; if ($request->hasHeader('Cache-Control')) { $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control')); if ($reqCacheControl->has('no-cache')) { // Can't return cache return null; } $maxAge = $reqCacheControl->get('max-age', null); } elseif ($request->hasHeader('Pragma')) { $pragma = new KeyValueHttpHeader($request->getHeader('Pragma')); if ($pragma->has('no-cache')) { // Can't return cache return null; } } $cache = $this->storage->fetch($this->getCacheKey($request)); if ($cache !== null) { if ((string) $cache->getOriginalRequest()->getUri() !== (string) $request->getUri()) { return null; } if ($maxAge !== null) { if ($cache->getAge() > $maxAge) { // Cache entry is too old for the request requirements! return null; } } if (!$cache->isVaryEquals($request)) { return null; } } return $cache; }
/** * @param callable $handler * * @return callable */ public function __invoke(callable $handler) { return function (RequestInterface $request, array $options) use(&$handler) { if (!isset($this->httpMethods[strtoupper($request->getMethod())])) { // No caching for this method allowed return $handler($request, $options)->then(function (ResponseInterface $response) { return $response->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_MISS); }); } if ($request->hasHeader(self::HEADER_RE_VALIDATION)) { // It's a re-validation request, so bypass the cache! return $handler($request->withoutHeader(self::HEADER_RE_VALIDATION), $options); } // Retrieve information from request (Cache-Control) $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control')); $onlyFromCache = $reqCacheControl->has('only-if-cached'); $staleResponse = $reqCacheControl->has('max-stale') && $reqCacheControl->get('max-stale') === ''; $maxStaleCache = $reqCacheControl->get('max-stale', null); $minFreshCache = $reqCacheControl->get('min-fresh', null); // If cache => return new FulfilledPromise(...) with response $cacheEntry = $this->cacheStorage->fetch($request); if ($cacheEntry instanceof CacheEntry) { if ($cacheEntry->isFresh() && ($minFreshCache === null || $cacheEntry->getStaleAge() + (int) $minFreshCache <= 0)) { // Cache HIT! return new FulfilledPromise($cacheEntry->getResponse()->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT)); } elseif ($staleResponse || $maxStaleCache !== null && $cacheEntry->getStaleAge() <= $maxStaleCache) { // Staled cache! return new FulfilledPromise($cacheEntry->getResponse()->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT)); } elseif ($cacheEntry->hasValidationInformation() && !$onlyFromCache) { // Re-validation header $request = static::getRequestWithReValidationHeader($request, $cacheEntry); if ($cacheEntry->staleWhileValidate()) { static::addReValidationRequest($request, $this->cacheStorage, $cacheEntry); return new FulfilledPromise($cacheEntry->getResponse()->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_STALE)); } } } else { $cacheEntry = null; } if ($cacheEntry === null && $onlyFromCache) { // Explicit asking of a cached response => 504 return new FulfilledPromise(new Response(504)); } /** @var Promise $promise */ $promise = $handler($request, $options); return $promise->then(function (ResponseInterface $response) use($request, $cacheEntry) { // Check if error and looking for a staled content if ($response->getStatusCode() >= 500) { $responseStale = static::getStaleResponse($cacheEntry); if ($responseStale instanceof ResponseInterface) { return $responseStale; } } if ($response->getStatusCode() == 304 && $cacheEntry instanceof CacheEntry) { // Not modified => cache entry is re-validate /** @var ResponseInterface $response */ $response = $response->withStatus($cacheEntry->getResponse()->getStatusCode())->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT); $response = $response->withBody($cacheEntry->getResponse()->getBody()); } else { $response = $response->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_MISS); } // Add to the cache $this->cacheStorage->cache($request, $response); return $response; }, function (\Exception $ex) use($cacheEntry) { if ($ex instanceof TransferException) { $response = static::getStaleResponse($cacheEntry); if ($response instanceof ResponseInterface) { return $response; } } throw $ex; }); }; }