CacheItem.php 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Cache;
  11. use Psr\Cache\CacheItemInterface;
  12. use Psr\Log\LoggerInterface;
  13. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  14. use Symfony\Component\Cache\Exception\LogicException;
  15. use Symfony\Contracts\Cache\ItemInterface;
  16. /**
  17. * @author Nicolas Grekas <p@tchwork.com>
  18. */
  19. final class CacheItem implements ItemInterface
  20. {
  21. private const METADATA_EXPIRY_OFFSET = 1527506807;
  22. private const VALUE_WRAPPER = "\xA9";
  23. protected string $key;
  24. protected mixed $value = null;
  25. protected bool $isHit = false;
  26. protected float|int|null $expiry = null;
  27. protected array $metadata = [];
  28. protected array $newMetadata = [];
  29. protected ?CacheItemInterface $innerItem = null;
  30. protected ?string $poolHash = null;
  31. protected bool $isTaggable = false;
  32. public function getKey(): string
  33. {
  34. return $this->key;
  35. }
  36. public function get(): mixed
  37. {
  38. return $this->value;
  39. }
  40. public function isHit(): bool
  41. {
  42. return $this->isHit;
  43. }
  44. /**
  45. * @return $this
  46. */
  47. public function set($value): static
  48. {
  49. $this->value = $value;
  50. return $this;
  51. }
  52. /**
  53. * @return $this
  54. */
  55. public function expiresAt(?\DateTimeInterface $expiration): static
  56. {
  57. $this->expiry = null !== $expiration ? (float) $expiration->format('U.u') : null;
  58. return $this;
  59. }
  60. /**
  61. * @return $this
  62. */
  63. public function expiresAfter(mixed $time): static
  64. {
  65. if (null === $time) {
  66. $this->expiry = null;
  67. } elseif ($time instanceof \DateInterval) {
  68. $this->expiry = microtime(true) + \DateTimeImmutable::createFromFormat('U', 0)->add($time)->format('U.u');
  69. } elseif (\is_int($time)) {
  70. $this->expiry = $time + microtime(true);
  71. } else {
  72. throw new InvalidArgumentException(\sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given.', get_debug_type($time)));
  73. }
  74. return $this;
  75. }
  76. public function tag(mixed $tags): static
  77. {
  78. if (!$this->isTaggable) {
  79. throw new LogicException(\sprintf('Cache item "%s" comes from a non tag-aware pool: you cannot tag it.', $this->key));
  80. }
  81. if (!\is_array($tags) && !$tags instanceof \Traversable) { // don't use is_iterable(), it's slow
  82. $tags = [$tags];
  83. }
  84. foreach ($tags as $tag) {
  85. if (!\is_string($tag) && !$tag instanceof \Stringable) {
  86. throw new InvalidArgumentException(\sprintf('Cache tag must be string or object that implements __toString(), "%s" given.', get_debug_type($tag)));
  87. }
  88. $tag = (string) $tag;
  89. if (isset($this->newMetadata[self::METADATA_TAGS][$tag])) {
  90. continue;
  91. }
  92. if ('' === $tag) {
  93. throw new InvalidArgumentException('Cache tag length must be greater than zero.');
  94. }
  95. if (false !== strpbrk($tag, self::RESERVED_CHARACTERS)) {
  96. throw new InvalidArgumentException(\sprintf('Cache tag "%s" contains reserved characters "%s".', $tag, self::RESERVED_CHARACTERS));
  97. }
  98. $this->newMetadata[self::METADATA_TAGS][$tag] = $tag;
  99. }
  100. return $this;
  101. }
  102. public function getMetadata(): array
  103. {
  104. return $this->metadata;
  105. }
  106. /**
  107. * Validates a cache key according to PSR-6.
  108. *
  109. * @param mixed $key The key to validate
  110. *
  111. * @throws InvalidArgumentException When $key is not valid
  112. */
  113. public static function validateKey($key): string
  114. {
  115. if (!\is_string($key)) {
  116. throw new InvalidArgumentException(\sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
  117. }
  118. if ('' === $key) {
  119. throw new InvalidArgumentException('Cache key length must be greater than zero.');
  120. }
  121. if (false !== strpbrk($key, self::RESERVED_CHARACTERS)) {
  122. throw new InvalidArgumentException(\sprintf('Cache key "%s" contains reserved characters "%s".', $key, self::RESERVED_CHARACTERS));
  123. }
  124. return $key;
  125. }
  126. /**
  127. * Internal logging helper.
  128. *
  129. * @internal
  130. */
  131. public static function log(?LoggerInterface $logger, string $message, array $context = []): void
  132. {
  133. if ($logger) {
  134. $logger->warning($message, $context);
  135. } else {
  136. $replace = [];
  137. foreach ($context as $k => $v) {
  138. if (\is_scalar($v)) {
  139. $replace['{'.$k.'}'] = $v;
  140. }
  141. }
  142. @trigger_error(strtr($message, $replace), \E_USER_WARNING);
  143. }
  144. }
  145. private function pack(): mixed
  146. {
  147. if (!$m = $this->newMetadata) {
  148. return $this->value;
  149. }
  150. $valueWrapper = self::VALUE_WRAPPER;
  151. return new $valueWrapper($this->value, $m + ['expiry' => $this->expiry]);
  152. }
  153. private function unpack(): bool
  154. {
  155. $v = $this->value;
  156. $valueWrapper = self::VALUE_WRAPPER;
  157. if ($v instanceof $valueWrapper) {
  158. $this->value = $v->value;
  159. $this->metadata = $v->metadata;
  160. return true;
  161. }
  162. if (!\is_array($v) || 1 !== \count($v) || 10 !== \strlen($k = (string) array_key_first($v)) || "\x9D" !== $k[0] || "\0" !== $k[5] || "\x5F" !== $k[9]) {
  163. return false;
  164. }
  165. // BC with pools populated before v6.1
  166. $this->value = $v[$k];
  167. $this->metadata = unpack('Vexpiry/Nctime', substr($k, 1, -1));
  168. $this->metadata['expiry'] += self::METADATA_EXPIRY_OFFSET;
  169. return true;
  170. }
  171. }
  172. // @php-cs-fixer-ignore protected_to_private Friend-level scope access relies on protected properties