vendor/symfony/cache/Adapter/TagAwareAdapter.php line 239

Open in your IDE?
  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\Adapter;
  11. use Psr\Cache\CacheItemInterface;
  12. use Psr\Cache\InvalidArgumentException;
  13. use Psr\Log\LoggerAwareInterface;
  14. use Psr\Log\LoggerAwareTrait;
  15. use Symfony\Component\Cache\CacheItem;
  16. use Symfony\Component\Cache\PruneableInterface;
  17. use Symfony\Component\Cache\ResettableInterface;
  18. use Symfony\Component\Cache\Traits\ContractsTrait;
  19. use Symfony\Contracts\Cache\TagAwareCacheInterface;
  20. /**
  21.  * Implements simple and robust tag-based invalidation suitable for use with volatile caches.
  22.  *
  23.  * This adapter works by storing a version for each tags. When saving an item, it is stored together with its tags and
  24.  * their corresponding versions. When retrieving an item, those tag versions are compared to the current version of
  25.  * each tags. Invalidation is achieved by deleting tags, thereby ensuring that their versions change even when the
  26.  * storage is out of space. When versions of non-existing tags are requested for item commits, this adapter assigns a
  27.  * new random version to them.
  28.  *
  29.  * @author Nicolas Grekas <p@tchwork.com>
  30.  * @author Sergey Belyshkin <sbelyshkin@gmail.com>
  31.  */
  32. class TagAwareAdapter implements TagAwareAdapterInterfaceTagAwareCacheInterfacePruneableInterfaceResettableInterfaceLoggerAwareInterface
  33. {
  34.     use ContractsTrait;
  35.     use LoggerAwareTrait;
  36.     public const TAGS_PREFIX "\1tags\1";
  37.     private array $deferred = [];
  38.     private AdapterInterface $pool;
  39.     private AdapterInterface $tags;
  40.     private array $knownTagVersions = [];
  41.     private float $knownTagVersionsTtl;
  42.     private static \Closure $setCacheItemTags;
  43.     private static \Closure $setTagVersions;
  44.     private static \Closure $getTagsByKey;
  45.     private static \Closure $saveTags;
  46.     public function __construct(AdapterInterface $itemsPoolAdapterInterface $tagsPool nullfloat $knownTagVersionsTtl 0.15)
  47.     {
  48.         $this->pool $itemsPool;
  49.         $this->tags $tagsPool ?? $itemsPool;
  50.         $this->knownTagVersionsTtl $knownTagVersionsTtl;
  51.         self::$setCacheItemTags ??= \Closure::bind(
  52.             static function (array $items, array $itemTags) {
  53.                 foreach ($items as $key => $item) {
  54.                     $item->isTaggable true;
  55.                     if (isset($itemTags[$key])) {
  56.                         $tags array_keys($itemTags[$key]);
  57.                         $item->metadata[CacheItem::METADATA_TAGS] = array_combine($tags$tags);
  58.                     } else {
  59.                         $item->value null;
  60.                         $item->isHit false;
  61.                         $item->metadata = [];
  62.                     }
  63.                 }
  64.                 return $items;
  65.             },
  66.             null,
  67.             CacheItem::class
  68.         );
  69.         self::$setTagVersions ??= \Closure::bind(
  70.             static function (array $items, array $tagVersions) {
  71.                 foreach ($items as $item) {
  72.                     $item->newMetadata[CacheItem::METADATA_TAGS] = array_intersect_key($tagVersions$item->newMetadata[CacheItem::METADATA_TAGS] ?? []);
  73.                 }
  74.             },
  75.             null,
  76.             CacheItem::class
  77.         );
  78.         self::$getTagsByKey ??= \Closure::bind(
  79.             static function ($deferred) {
  80.                 $tagsByKey = [];
  81.                 foreach ($deferred as $key => $item) {
  82.                     $tagsByKey[$key] = $item->newMetadata[CacheItem::METADATA_TAGS] ?? [];
  83.                     $item->metadata $item->newMetadata;
  84.                 }
  85.                 return $tagsByKey;
  86.             },
  87.             null,
  88.             CacheItem::class
  89.         );
  90.         self::$saveTags ??= \Closure::bind(
  91.             static function (AdapterInterface $tagsAdapter, array $tags) {
  92.                 ksort($tags);
  93.                 foreach ($tags as $v) {
  94.                     $v->expiry 0;
  95.                     $tagsAdapter->saveDeferred($v);
  96.                 }
  97.                 return $tagsAdapter->commit();
  98.             },
  99.             null,
  100.             CacheItem::class
  101.         );
  102.     }
  103.     public function invalidateTags(array $tags): bool
  104.     {
  105.         $ids = [];
  106.         foreach ($tags as $tag) {
  107.             \assert('' !== CacheItem::validateKey($tag));
  108.             unset($this->knownTagVersions[$tag]);
  109.             $ids[] = $tag.static::TAGS_PREFIX;
  110.         }
  111.         return !$tags || $this->tags->deleteItems($ids);
  112.     }
  113.     public function hasItem(mixed $key): bool
  114.     {
  115.         return $this->getItem($key)->isHit();
  116.     }
  117.     public function getItem(mixed $key): CacheItem
  118.     {
  119.         foreach ($this->getItems([$key]) as $item) {
  120.             return $item;
  121.         }
  122.     }
  123.     public function getItems(array $keys = []): iterable
  124.     {
  125.         $tagKeys = [];
  126.         $commit false;
  127.         foreach ($keys as $key) {
  128.             if ('' !== $key && \is_string($key)) {
  129.                 $commit $commit || isset($this->deferred[$key]);
  130.                 $key = static::TAGS_PREFIX.$key;
  131.                 $tagKeys[$key] = $key// BC with pools populated before v6.1
  132.             }
  133.         }
  134.         if ($commit) {
  135.             $this->commit();
  136.         }
  137.         try {
  138.             $items $this->pool->getItems($tagKeys $keys);
  139.         } catch (InvalidArgumentException $e) {
  140.             $this->pool->getItems($keys); // Should throw an exception
  141.             throw $e;
  142.         }
  143.         $bufferedItems $itemTags = [];
  144.         foreach ($items as $key => $item) {
  145.             if (isset($tagKeys[$key])) { // BC with pools populated before v6.1
  146.                 if ($item->isHit()) {
  147.                     $itemTags[substr($key\strlen(static::TAGS_PREFIX))] = $item->get() ?: [];
  148.                 }
  149.                 continue;
  150.             }
  151.             if (null !== $tags $item->getMetadata()[CacheItem::METADATA_TAGS] ?? null) {
  152.                 $itemTags[$key] = $tags;
  153.             }
  154.             $bufferedItems[$key] = $item;
  155.         }
  156.         $tagVersions $this->getTagVersions($itemTagsfalse);
  157.         foreach ($itemTags as $key => $tags) {
  158.             foreach ($tags as $tag => $version) {
  159.                 if ($tagVersions[$tag] !== $version) {
  160.                     unset($itemTags[$key]);
  161.                     continue 2;
  162.                 }
  163.             }
  164.         }
  165.         $tagVersions null;
  166.         return (self::$setCacheItemTags)($bufferedItems$itemTags);
  167.     }
  168.     public function clear(string $prefix ''): bool
  169.     {
  170.         if ('' !== $prefix) {
  171.             foreach ($this->deferred as $key => $item) {
  172.                 if (str_starts_with($key$prefix)) {
  173.                     unset($this->deferred[$key]);
  174.                 }
  175.             }
  176.         } else {
  177.             $this->deferred = [];
  178.         }
  179.         if ($this->pool instanceof AdapterInterface) {
  180.             return $this->pool->clear($prefix);
  181.         }
  182.         return $this->pool->clear();
  183.     }
  184.     public function deleteItem(mixed $key): bool
  185.     {
  186.         return $this->deleteItems([$key]);
  187.     }
  188.     public function deleteItems(array $keys): bool
  189.     {
  190.         foreach ($keys as $key) {
  191.             if ('' !== $key && \is_string($key)) {
  192.                 $keys[] = static::TAGS_PREFIX.$key// BC with pools populated before v6.1
  193.             }
  194.         }
  195.         return $this->pool->deleteItems($keys);
  196.     }
  197.     public function save(CacheItemInterface $item): bool
  198.     {
  199.         if (!$item instanceof CacheItem) {
  200.             return false;
  201.         }
  202.         $this->deferred[$item->getKey()] = $item;
  203.         return $this->commit();
  204.     }
  205.     public function saveDeferred(CacheItemInterface $item): bool
  206.     {
  207.         if (!$item instanceof CacheItem) {
  208.             return false;
  209.         }
  210.         $this->deferred[$item->getKey()] = $item;
  211.         return true;
  212.     }
  213.     public function commit(): bool
  214.     {
  215.         if (!$items $this->deferred) {
  216.             return true;
  217.         }
  218.         $tagVersions $this->getTagVersions((self::$getTagsByKey)($items), true);
  219.         (self::$setTagVersions)($items$tagVersions);
  220.         $ok true;
  221.         foreach ($items as $key => $item) {
  222.             if ($this->pool->saveDeferred($item)) {
  223.                 unset($this->deferred[$key]);
  224.             } else {
  225.                 $ok false;
  226.             }
  227.         }
  228.         $ok $this->pool->commit() && $ok;
  229.         $tagVersions array_keys($tagVersions);
  230.         (self::$setTagVersions)($itemsarray_combine($tagVersions$tagVersions));
  231.         return $ok;
  232.     }
  233.     public function prune(): bool
  234.     {
  235.         return $this->pool instanceof PruneableInterface && $this->pool->prune();
  236.     }
  237.     /**
  238.      * @return void
  239.      */
  240.     public function reset()
  241.     {
  242.         $this->commit();
  243.         $this->knownTagVersions = [];
  244.         $this->pool instanceof ResettableInterface && $this->pool->reset();
  245.         $this->tags instanceof ResettableInterface && $this->tags->reset();
  246.     }
  247.     public function __sleep(): array
  248.     {
  249.         throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
  250.     }
  251.     public function __wakeup()
  252.     {
  253.         throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
  254.     }
  255.     public function __destruct()
  256.     {
  257.         $this->commit();
  258.     }
  259.     private function getTagVersions(array $tagsByKeybool $persistTags): array
  260.     {
  261.         $tagVersions = [];
  262.         $fetchTagVersions $persistTags;
  263.         foreach ($tagsByKey as $tags) {
  264.             $tagVersions += $tags;
  265.             if ($fetchTagVersions) {
  266.                 continue;
  267.             }
  268.             foreach ($tags as $tag => $version) {
  269.                 if ($tagVersions[$tag] !== $version) {
  270.                     $fetchTagVersions true;
  271.                 }
  272.             }
  273.         }
  274.         if (!$tagVersions) {
  275.             return [];
  276.         }
  277.         $now microtime(true);
  278.         $tags = [];
  279.         foreach ($tagVersions as $tag => $version) {
  280.             $tags[$tag.static::TAGS_PREFIX] = $tag;
  281.             $knownTagVersion $this->knownTagVersions[$tag] ?? [0null];
  282.             if ($fetchTagVersions || $now $knownTagVersion[0] || $knownTagVersion[1] !== $version) {
  283.                 // reuse previously fetched tag versions until the expiration
  284.                 $fetchTagVersions true;
  285.             }
  286.         }
  287.         if (!$fetchTagVersions) {
  288.             return $tagVersions;
  289.         }
  290.         $newTags = [];
  291.         $newVersion null;
  292.         $expiration $now $this->knownTagVersionsTtl;
  293.         foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) {
  294.             unset($this->knownTagVersions[$tag $tags[$tag]]); // update FIFO
  295.             if (null !== $tagVersions[$tag] = $version->get()) {
  296.                 $this->knownTagVersions[$tag] = [$expiration$tagVersions[$tag]];
  297.             } elseif ($persistTags) {
  298.                 $newTags[$tag] = $version->set($newVersion ??= random_bytes(6));
  299.                 $tagVersions[$tag] = $newVersion;
  300.                 $this->knownTagVersions[$tag] = [$expiration$newVersion];
  301.             }
  302.         }
  303.         if ($newTags) {
  304.             (self::$saveTags)($this->tags$newTags);
  305.         }
  306.         while ($now > ($this->knownTagVersions[$tag array_key_first($this->knownTagVersions)][0] ?? \INF)) {
  307.             unset($this->knownTagVersions[$tag]);
  308.         }
  309.         return $tagVersions;
  310.     }
  311. }