vendor/symfony/cache/Traits/AbstractAdapterTrait.php line 51

  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\Traits;
  11. use Psr\Cache\CacheItemInterface;
  12. use Psr\Log\LoggerAwareTrait;
  13. use Symfony\Component\Cache\CacheItem;
  14. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  15. /**
  16.  * @author Nicolas Grekas <p@tchwork.com>
  17.  *
  18.  * @internal
  19.  */
  20. trait AbstractAdapterTrait
  21. {
  22.     use LoggerAwareTrait;
  23.     /**
  24.      * needs to be set by class, signature is function(string <key>, mixed <value>, bool <isHit>).
  25.      */
  26.     private static \Closure $createCacheItem;
  27.     /**
  28.      * needs to be set by class, signature is function(array <deferred>, string <namespace>, array <&expiredIds>).
  29.      */
  30.     private static \Closure $mergeByLifetime;
  31.     private readonly string $rootNamespace;
  32.     private string $namespace '';
  33.     private int $defaultLifetime;
  34.     private string $namespaceVersion '';
  35.     private bool $versioningIsEnabled false;
  36.     private array $deferred = [];
  37.     private array $ids = [];
  38.     /**
  39.      * The maximum length to enforce for identifiers or null when no limit applies.
  40.      */
  41.     protected ?int $maxIdLength null;
  42.     /**
  43.      * Fetches several cache items.
  44.      *
  45.      * @param array $ids The cache identifiers to fetch
  46.      */
  47.     abstract protected function doFetch(array $ids): iterable;
  48.     /**
  49.      * Confirms if the cache contains specified cache item.
  50.      *
  51.      * @param string $id The identifier for which to check existence
  52.      */
  53.     abstract protected function doHave(string $id): bool;
  54.     /**
  55.      * Deletes all items in the pool.
  56.      *
  57.      * @param string $namespace The prefix used for all identifiers managed by this pool
  58.      */
  59.     abstract protected function doClear(string $namespace): bool;
  60.     /**
  61.      * Removes multiple items from the pool.
  62.      *
  63.      * @param array $ids An array of identifiers that should be removed from the pool
  64.      */
  65.     abstract protected function doDelete(array $ids): bool;
  66.     /**
  67.      * Persists several cache items immediately.
  68.      *
  69.      * @param array $values   The values to cache, indexed by their cache identifier
  70.      * @param int   $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning
  71.      *
  72.      * @return array|bool The identifiers that failed to be cached or a boolean stating if caching succeeded or not
  73.      */
  74.     abstract protected function doSave(array $valuesint $lifetime): array|bool;
  75.     public function hasItem(mixed $key): bool
  76.     {
  77.         $id $this->getId($key);
  78.         if (isset($this->deferred[$key])) {
  79.             $this->commit();
  80.         }
  81.         try {
  82.             return $this->doHave($id);
  83.         } catch (\Exception $e) {
  84.             CacheItem::log($this->logger'Failed to check if key "{key}" is cached: '.$e->getMessage(), ['key' => $key'exception' => $e'cache-adapter' => get_debug_type($this)]);
  85.             return false;
  86.         }
  87.     }
  88.     public function clear(string $prefix ''): bool
  89.     {
  90.         $this->deferred = [];
  91.         if ($cleared $this->versioningIsEnabled) {
  92.             $rootNamespace $this->rootNamespace ??= $this->namespace;
  93.             if ('' === $namespaceVersionToClear $this->namespaceVersion) {
  94.                 foreach ($this->doFetch([static::NS_SEPARATOR.$rootNamespace]) as $v) {
  95.                     $namespaceVersionToClear $v;
  96.                 }
  97.             }
  98.             $namespaceToClear $rootNamespace.$namespaceVersionToClear;
  99.             $namespaceVersion self::formatNamespaceVersion(mt_rand());
  100.             try {
  101.                 $e $this->doSave([static::NS_SEPARATOR.$rootNamespace => $namespaceVersion], 0);
  102.             } catch (\Exception $e) {
  103.             }
  104.             if (true !== $e && [] !== $e) {
  105.                 $cleared false;
  106.                 $message 'Failed to save the new namespace'.($e instanceof \Exception ': '.$e->getMessage() : '.');
  107.                 CacheItem::log($this->logger$message, ['exception' => $e instanceof \Exception $e null'cache-adapter' => get_debug_type($this)]);
  108.             } else {
  109.                 $this->namespaceVersion $namespaceVersion;
  110.                 $this->ids = [];
  111.             }
  112.         } else {
  113.             $namespaceToClear $this->namespace.$prefix;
  114.         }
  115.         try {
  116.             return $this->doClear($namespaceToClear) || $cleared;
  117.         } catch (\Exception $e) {
  118.             CacheItem::log($this->logger'Failed to clear the cache: '.$e->getMessage(), ['exception' => $e'cache-adapter' => get_debug_type($this)]);
  119.             return false;
  120.         }
  121.     }
  122.     public function deleteItem(mixed $key): bool
  123.     {
  124.         return $this->deleteItems([$key]);
  125.     }
  126.     public function deleteItems(array $keys): bool
  127.     {
  128.         $ids = [];
  129.         foreach ($keys as $key) {
  130.             $ids[$key] = $this->getId($key);
  131.             unset($this->deferred[$key]);
  132.         }
  133.         try {
  134.             if ($this->doDelete($ids)) {
  135.                 return true;
  136.             }
  137.         } catch (\Exception) {
  138.         }
  139.         $ok true;
  140.         // When bulk-delete failed, retry each item individually
  141.         foreach ($ids as $key => $id) {
  142.             try {
  143.                 $e null;
  144.                 if ($this->doDelete([$id])) {
  145.                     continue;
  146.                 }
  147.             } catch (\Exception $e) {
  148.             }
  149.             $message 'Failed to delete key "{key}"'.($e instanceof \Exception ': '.$e->getMessage() : '.');
  150.             CacheItem::log($this->logger$message, ['key' => $key'exception' => $e'cache-adapter' => get_debug_type($this)]);
  151.             $ok false;
  152.         }
  153.         return $ok;
  154.     }
  155.     public function getItem(mixed $key): CacheItem
  156.     {
  157.         $id $this->getId($key);
  158.         if (isset($this->deferred[$key])) {
  159.             $this->commit();
  160.         }
  161.         $isHit false;
  162.         $value null;
  163.         try {
  164.             foreach ($this->doFetch([$id]) as $value) {
  165.                 $isHit true;
  166.             }
  167.             return (self::$createCacheItem)($key$value$isHit);
  168.         } catch (\Exception $e) {
  169.             CacheItem::log($this->logger'Failed to fetch key "{key}": '.$e->getMessage(), ['key' => $key'exception' => $e'cache-adapter' => get_debug_type($this)]);
  170.         }
  171.         return (self::$createCacheItem)($keynullfalse);
  172.     }
  173.     public function getItems(array $keys = []): iterable
  174.     {
  175.         $ids = [];
  176.         $commit false;
  177.         foreach ($keys as $key) {
  178.             $ids[] = $this->getId($key);
  179.             $commit $commit || isset($this->deferred[$key]);
  180.         }
  181.         if ($commit) {
  182.             $this->commit();
  183.         }
  184.         try {
  185.             $items $this->doFetch($ids);
  186.         } catch (\Exception $e) {
  187.             CacheItem::log($this->logger'Failed to fetch items: '.$e->getMessage(), ['keys' => $keys'exception' => $e'cache-adapter' => get_debug_type($this)]);
  188.             $items = [];
  189.         }
  190.         $ids array_combine($ids$keys);
  191.         return $this->generateItems($items$ids);
  192.     }
  193.     public function save(CacheItemInterface $item): bool
  194.     {
  195.         if (!$item instanceof CacheItem) {
  196.             return false;
  197.         }
  198.         $this->deferred[$item->getKey()] = $item;
  199.         return $this->commit();
  200.     }
  201.     public function saveDeferred(CacheItemInterface $item): bool
  202.     {
  203.         if (!$item instanceof CacheItem) {
  204.             return false;
  205.         }
  206.         $this->deferred[$item->getKey()] = $item;
  207.         return true;
  208.     }
  209.     public function withSubNamespace(string $namespace): static
  210.     {
  211.         $this->rootNamespace ??= $this->namespace;
  212.         $clone = clone $this;
  213.         $clone->namespace .= CacheItem::validateKey($namespace).static::NS_SEPARATOR;
  214.         return $clone;
  215.     }
  216.     /**
  217.      * Enables/disables versioning of items.
  218.      *
  219.      * When versioning is enabled, clearing the cache is atomic and doesn't require listing existing keys to proceed,
  220.      * but old keys may need garbage collection and extra round-trips to the back-end are required.
  221.      *
  222.      * Calling this method also clears the memoized namespace version and thus forces a resynchronization of it.
  223.      *
  224.      * @return bool the previous state of versioning
  225.      */
  226.     public function enableVersioning(bool $enable true): bool
  227.     {
  228.         $wasEnabled $this->versioningIsEnabled;
  229.         $this->versioningIsEnabled $enable;
  230.         $this->namespaceVersion '';
  231.         $this->ids = [];
  232.         return $wasEnabled;
  233.     }
  234.     public function reset(): void
  235.     {
  236.         if ($this->deferred) {
  237.             $this->commit();
  238.         }
  239.         $this->namespaceVersion '';
  240.         $this->ids = [];
  241.     }
  242.     public function __serialize(): array
  243.     {
  244.         throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
  245.     }
  246.     public function __unserialize(array $data): void
  247.     {
  248.         throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
  249.     }
  250.     public function __destruct()
  251.     {
  252.         if ($this->deferred) {
  253.             $this->commit();
  254.         }
  255.     }
  256.     private function generateItems(iterable $items, array &$keys): \Generator
  257.     {
  258.         $f self::$createCacheItem;
  259.         try {
  260.             foreach ($items as $id => $value) {
  261.                 if (!isset($keys[$id])) {
  262.                     throw new InvalidArgumentException(\sprintf('Could not match value id "%s" to keys "%s".'$idimplode('", "'$keys)));
  263.                 }
  264.                 $key $keys[$id];
  265.                 unset($keys[$id]);
  266.                 yield $key => $f($key$valuetrue);
  267.             }
  268.         } catch (\Exception $e) {
  269.             CacheItem::log($this->logger'Failed to fetch items: '.$e->getMessage(), ['keys' => array_values($keys), 'exception' => $e'cache-adapter' => get_debug_type($this)]);
  270.         }
  271.         foreach ($keys as $key) {
  272.             yield $key => $f($keynullfalse);
  273.         }
  274.     }
  275.     /**
  276.      * @internal
  277.      */
  278.     protected function getId(mixed $key, ?string $namespace null): string
  279.     {
  280.         $namespace ??= $this->namespace;
  281.         if ('' !== $this->namespaceVersion) {
  282.             $namespace .= $this->namespaceVersion;
  283.         } elseif ($this->versioningIsEnabled) {
  284.             $rootNamespace $this->rootNamespace ??= $this->namespace;
  285.             $this->ids = [];
  286.             $this->namespaceVersion '1'.static::NS_SEPARATOR;
  287.             try {
  288.                 foreach ($this->doFetch([static::NS_SEPARATOR.$rootNamespace]) as $v) {
  289.                     $this->namespaceVersion $v;
  290.                 }
  291.                 $e true;
  292.                 if ('1'.static::NS_SEPARATOR === $this->namespaceVersion) {
  293.                     $this->namespaceVersion self::formatNamespaceVersion(time());
  294.                     $e $this->doSave([static::NS_SEPARATOR.$rootNamespace => $this->namespaceVersion], 0);
  295.                 }
  296.             } catch (\Exception $e) {
  297.             }
  298.             if (true !== $e && [] !== $e) {
  299.                 $message 'Failed to save the new namespace'.($e instanceof \Exception ': '.$e->getMessage() : '.');
  300.                 CacheItem::log($this->logger$message, ['exception' => $e instanceof \Exception $e null'cache-adapter' => get_debug_type($this)]);
  301.             }
  302.             $namespace .= $this->namespaceVersion;
  303.         }
  304.         if (\is_string($key) && isset($this->ids[$key])) {
  305.             $id $this->ids[$key];
  306.         } else {
  307.             \assert('' !== CacheItem::validateKey($key));
  308.             $this->ids[$key] = $key;
  309.             if (\count($this->ids) > 1000) {
  310.                 $this->ids \array_slice($this->ids500nulltrue); // stop memory leak if there are many keys
  311.             }
  312.             if (null === $this->maxIdLength) {
  313.                 return $namespace.$key;
  314.             }
  315.             if (\strlen($id $namespace.$key) <= $this->maxIdLength) {
  316.                 return $id;
  317.             }
  318.             // Use xxh128 to favor speed over security, which is not an issue here
  319.             $this->ids[$key] = $id substr_replace(base64_encode(hash('xxh128'$keytrue)), static::NS_SEPARATOR, -(\strlen($this->namespaceVersion) + 2));
  320.         }
  321.         $id $namespace.$id;
  322.         if (null !== $this->maxIdLength && \strlen($id) > $this->maxIdLength) {
  323.             return base64_encode(hash('xxh128'$idtrue));
  324.         }
  325.         return $id;
  326.     }
  327.     /**
  328.      * @internal
  329.      */
  330.     public static function handleUnserializeCallback(string $class): never
  331.     {
  332.         throw new \DomainException('Class not found: '.$class);
  333.     }
  334.     private static function formatNamespaceVersion(int $value): string
  335.     {
  336.         return strtr(substr_replace(base64_encode(pack('V'$value)), static::NS_SEPARATOR5), '/''_');
  337.     }
  338. }