Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
   1  <?php
   2  
   3  namespace Kevinrob\GuzzleCache;
   4  
   5  use GuzzleHttp\Psr7\PumpStream;
   6  use Psr\Http\Message\RequestInterface;
   7  use Psr\Http\Message\ResponseInterface;
   8  
   9  class CacheEntry
  10  {
  11      /**
  12       * @var RequestInterface
  13       */
  14      protected $request;
  15  
  16      /**
  17       * @var ResponseInterface
  18       */
  19      protected $response;
  20  
  21      /**
  22       * @var \DateTime
  23       */
  24      protected $staleAt;
  25  
  26      /**
  27       * @var \DateTime
  28       */
  29      protected $staleIfErrorTo;
  30  
  31      /**
  32       * @var \DateTime
  33       */
  34      protected $staleWhileRevalidateTo;
  35  
  36      /**
  37       * @var \DateTime
  38       */
  39      protected $dateCreated;
  40  
  41      /**
  42       * Cached timestamp of staleAt variable.
  43       *
  44       * @var int
  45       */
  46      protected $timestampStale;
  47  
  48      /**
  49       * @param RequestInterface $request
  50       * @param ResponseInterface $response
  51       * @param \DateTime $staleAt
  52       * @param \DateTime|null $staleIfErrorTo if null, detected with the headers (RFC 5861)
  53       * @param \DateTime|null $staleWhileRevalidateTo
  54       */
  55      public function __construct(
  56          RequestInterface $request,
  57          ResponseInterface $response,
  58          \DateTime $staleAt,
  59          \DateTime $staleIfErrorTo = null,
  60          \DateTime $staleWhileRevalidateTo = null
  61      ) {
  62          $this->dateCreated = new \DateTime();
  63  
  64          $this->request = $request;
  65          $this->response = $response;
  66          $this->staleAt = $staleAt;
  67  
  68          $values = new KeyValueHttpHeader($response->getHeader('Cache-Control'));
  69  
  70          if ($staleIfErrorTo === null && $values->has('stale-if-error')) {
  71              $this->staleIfErrorTo = (new \DateTime(
  72                  '@'.($this->staleAt->getTimestamp() + (int) $values->get('stale-if-error'))
  73              ));
  74          } else {
  75              $this->staleIfErrorTo = $staleIfErrorTo;
  76          }
  77  
  78          if ($staleWhileRevalidateTo === null && $values->has('stale-while-revalidate')) {
  79              $this->staleWhileRevalidateTo = new \DateTime(
  80                  '@'.($this->staleAt->getTimestamp() + (int) $values->get('stale-while-revalidate'))
  81              );
  82          } else {
  83              $this->staleWhileRevalidateTo = $staleWhileRevalidateTo;
  84          }
  85      }
  86  
  87      /**
  88       * @return ResponseInterface
  89       */
  90      public function getResponse()
  91      {
  92          return $this->response
  93              ->withHeader('Age', $this->getAge());
  94      }
  95  
  96      /**
  97       * @return ResponseInterface
  98       */
  99      public function getOriginalResponse()
 100      {
 101          return $this->response;
 102      }
 103  
 104      /**
 105       * @return RequestInterface
 106       */
 107      public function getOriginalRequest()
 108      {
 109          return $this->request;
 110      }
 111  
 112      /**
 113       * @param RequestInterface $request
 114       * @return bool
 115       */
 116      public function isVaryEquals(RequestInterface $request)
 117      {
 118          if ($this->response->hasHeader('Vary')) {
 119              if ($this->request === null) {
 120                  return false;
 121              }
 122  
 123              foreach ($this->getVaryHeaders() as $key => $value) {
 124                  if (!$this->request->hasHeader($key)
 125                      && !$request->hasHeader($key)
 126                  ) {
 127                      // Absent from both
 128                      continue;
 129                  } elseif ($this->request->getHeaderLine($key)
 130                      == $request->getHeaderLine($key)
 131                  ) {
 132                      // Same content
 133                      continue;
 134                  }
 135  
 136                  return false;
 137              }
 138          }
 139  
 140          return true;
 141      }
 142  
 143      /**
 144       * Get the vary headers that should be honoured by the cache.
 145       *
 146       * @return KeyValueHttpHeader
 147       */
 148      public function getVaryHeaders()
 149      {
 150          return new KeyValueHttpHeader($this->response->getHeader('Vary'));
 151      }
 152  
 153      /**
 154       * @return \DateTime
 155       */
 156      public function getStaleAt()
 157      {
 158          return $this->staleAt;
 159      }
 160  
 161      /**
 162       * @return bool
 163       */
 164      public function isFresh()
 165      {
 166          return !$this->isStale();
 167      }
 168  
 169      /**
 170       * @return bool
 171       */
 172      public function isStale()
 173      {
 174          return $this->getStaleAge() > 0;
 175      }
 176  
 177      /**
 178       * @return int positive value equal staled
 179       */
 180      public function getStaleAge()
 181      {
 182          // This object is immutable
 183          if ($this->timestampStale === null) {
 184              $this->timestampStale = $this->staleAt->getTimestamp();
 185          }
 186  
 187          return time() - $this->timestampStale;
 188      }
 189  
 190      /**
 191       * @return bool
 192       */
 193      public function serveStaleIfError()
 194      {
 195          return $this->staleIfErrorTo !== null
 196              && $this->staleIfErrorTo->getTimestamp() >= (new \DateTime())->getTimestamp();
 197      }
 198  
 199      /**
 200       * @return bool
 201       */
 202      public function staleWhileValidate()
 203      {
 204          return $this->staleWhileRevalidateTo !== null
 205              && $this->staleWhileRevalidateTo->getTimestamp() >= (new \DateTime())->getTimestamp();
 206      }
 207  
 208      /**
 209       * @return bool
 210       */
 211      public function hasValidationInformation()
 212      {
 213          return $this->response->hasHeader('Etag') || $this->response->hasHeader('Last-Modified');
 214      }
 215  
 216      /**
 217       * Time in seconds how long the entry should be kept in the cache
 218       *
 219       * This will not give the time (in seconds) that the response will still be fresh for
 220       * from the HTTP point of view, but an upper bound on how long it is necessary and
 221       * reasonable to keep the response in a cache (to re-use it or re-validate it later on).
 222       *
 223       * @return int TTL in seconds (0 = infinite)
 224       */
 225      public function getTTL()
 226      {
 227          if ($this->hasValidationInformation()) {
 228              // No TTL if we have a way to re-validate the cache
 229              return 0;
 230          }
 231  
 232          $ttl = 0;
 233  
 234          // Keep it when stale if error
 235          if ($this->staleIfErrorTo !== null) {
 236              $ttl = max($ttl, $this->staleIfErrorTo->getTimestamp() - time());
 237          }
 238  
 239          // Keep it when stale-while-revalidate
 240          if ($this->staleWhileRevalidateTo !== null) {
 241              $ttl = max($ttl, $this->staleWhileRevalidateTo->getTimestamp() - time());
 242          }
 243  
 244          // Keep it until it become stale
 245          $ttl = max($ttl, $this->staleAt->getTimestamp() - time());
 246  
 247          // Don't return 0, it's reserved for infinite TTL
 248          return $ttl !== 0 ? (int) $ttl : -1;
 249      }
 250  
 251      /**
 252       * @return int Age in seconds
 253       */
 254      public function getAge()
 255      {
 256          return time() - $this->dateCreated->getTimestamp();
 257      }
 258  
 259      public function __sleep()
 260      {
 261          // Stream/Resource can't be serialized... So we copy the content into an implementation of `Psr\Http\Message\StreamInterface`
 262          if ($this->response !== null) {
 263              $responseBody = (string)$this->response->getBody();
 264              $this->response = $this->response->withBody(
 265                  new PumpStream(
 266                      new BodyStore($responseBody),
 267                      [
 268                          'size' => mb_strlen($responseBody),
 269                      ]
 270                  )
 271              );
 272          }
 273  
 274          $requestBody = (string)$this->request->getBody();
 275          $this->request = $this->request->withBody(
 276              new PumpStream(
 277                  new BodyStore($requestBody),
 278                  [
 279                      'size' => mb_strlen($requestBody)
 280                  ]
 281              )
 282          );
 283  
 284          return array_keys(get_object_vars($this));
 285      }
 286  
 287      public function __wakeup()
 288      {
 289          // We re-create the stream of the response
 290          if ($this->response !== null) {
 291              $this->response = $this->response
 292                  ->withBody(
 293                      \GuzzleHttp\Psr7\Utils::streamFor((string) $this->response->getBody())
 294                  );
 295          }
 296          $this->request = $this->request
 297              ->withBody(
 298                  \GuzzleHttp\Psr7\Utils::streamFor((string) $this->request->getBody())
 299              );
 300      }
 301  
 302  }