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

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 "\0tags\0";
  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.     /**
  104.      * {@inheritdoc}
  105.      */
  106.     public function invalidateTags(array $tags): bool
  107.     {
  108.         $ids = [];
  109.         foreach ($tags as $tag) {
  110.             \assert('' !== CacheItem::validateKey($tag));
  111.             unset($this->knownTagVersions[$tag]);
  112.             $ids[] = $tag.static::TAGS_PREFIX;
  113.         }
  114.         return !$tags || $this->tags->deleteItems($ids);
  115.     }
  116.     /**
  117.      * {@inheritdoc}
  118.      */
  119.     public function hasItem(mixed $key): bool
  120.     {
  121.         return $this->getItem($key)->isHit();
  122.     }
  123.     /**
  124.      * {@inheritdoc}
  125.      */
  126.     public function getItem(mixed $key): CacheItem
  127.     {
  128.         foreach ($this->getItems([$key]) as $item) {
  129.             return $item;
  130.         }
  131.     }
  132.     /**
  133.      * {@inheritdoc}
  134.      */
  135.     public function getItems(array $keys = []): iterable
  136.     {
  137.         $tagKeys = [];
  138.         $commit false;
  139.         foreach ($keys as $key) {
  140.             if ('' !== $key && \is_string($key)) {
  141.                 $commit $commit || isset($this->deferred[$key]);
  142.                 $key = static::TAGS_PREFIX.$key;
  143.                 $tagKeys[$key] = $key// BC with pools populated before v6.1
  144.             }
  145.         }
  146.         if ($commit) {
  147.             $this->commit();
  148.         }
  149.         try {
  150.             $items $this->pool->getItems($tagKeys $keys);
  151.         } catch (InvalidArgumentException $e) {
  152.             $this->pool->getItems($keys); // Should throw an exception
  153.             throw $e;
  154.         }
  155.         $bufferedItems $itemTags = [];
  156.         foreach ($items as $key => $item) {
  157.             if (isset($tagKeys[$key])) { // BC with pools populated before v6.1
  158.                 if ($item->isHit()) {
  159.                     $itemTags[substr($key\strlen(static::TAGS_PREFIX))] = $item->get() ?: [];
  160.                 }
  161.                 continue;
  162.             }
  163.             if (null !== $tags $item->getMetadata()[CacheItem::METADATA_TAGS] ?? null) {
  164.                 $itemTags[$key] = $tags;
  165.             }
  166.             $bufferedItems[$key] = $item;
  167.         }
  168.         $tagVersions $this->getTagVersions($itemTagsfalse);
  169.         foreach ($itemTags as $key => $tags) {
  170.             foreach ($tags as $tag => $version) {
  171.                 if ($tagVersions[$tag] !== $version) {
  172.                     unset($itemTags[$key]);
  173.                     continue 2;
  174.                 }
  175.             }
  176.         }
  177.         $tagVersions null;
  178.         return (self::$setCacheItemTags)($bufferedItems$itemTags);
  179.     }
  180.     /**
  181.      * {@inheritdoc}
  182.      */
  183.     public function clear(string $prefix ''): bool
  184.     {
  185.         if ('' !== $prefix) {
  186.             foreach ($this->deferred as $key => $item) {
  187.                 if (str_starts_with($key$prefix)) {
  188.                     unset($this->deferred[$key]);
  189.                 }
  190.             }
  191.         } else {
  192.             $this->deferred = [];
  193.         }
  194.         if ($this->pool instanceof AdapterInterface) {
  195.             return $this->pool->clear($prefix);
  196.         }
  197.         return $this->pool->clear();
  198.     }
  199.     /**
  200.      * {@inheritdoc}
  201.      */
  202.     public function deleteItem(mixed $key): bool
  203.     {
  204.         return $this->deleteItems([$key]);
  205.     }
  206.     /**
  207.      * {@inheritdoc}
  208.      */
  209.     public function deleteItems(array $keys): bool
  210.     {
  211.         foreach ($keys as $key) {
  212.             if ('' !== $key && \is_string($key)) {
  213.                 $keys[] = static::TAGS_PREFIX.$key// BC with pools populated before v6.1
  214.             }
  215.         }
  216.         return $this->pool->deleteItems($keys);
  217.     }
  218.     /**
  219.      * {@inheritdoc}
  220.      */
  221.     public function save(CacheItemInterface $item): bool
  222.     {
  223.         if (!$item instanceof CacheItem) {
  224.             return false;
  225.         }
  226.         $this->deferred[$item->getKey()] = $item;
  227.         return $this->commit();
  228.     }
  229.     /**
  230.      * {@inheritdoc}
  231.      */
  232.     public function saveDeferred(CacheItemInterface $item): bool
  233.     {
  234.         if (!$item instanceof CacheItem) {
  235.             return false;
  236.         }
  237.         $this->deferred[$item->getKey()] = $item;
  238.         return true;
  239.     }
  240.     /**
  241.      * {@inheritdoc}
  242.      */
  243.     public function commit(): bool
  244.     {
  245.         if (!$items $this->deferred) {
  246.             return true;
  247.         }
  248.         $tagVersions $this->getTagVersions((self::$getTagsByKey)($items), true);
  249.         (self::$setTagVersions)($items$tagVersions);
  250.         $ok true;
  251.         foreach ($items as $key => $item) {
  252.             if ($this->pool->saveDeferred($item)) {
  253.                 unset($this->deferred[$key]);
  254.             } else {
  255.                 $ok false;
  256.             }
  257.         }
  258.         $ok $this->pool->commit() && $ok;
  259.         $tagVersions array_keys($tagVersions);
  260.         (self::$setTagVersions)($itemsarray_combine($tagVersions$tagVersions));
  261.         return $ok;
  262.     }
  263.     /**
  264.      * {@inheritdoc}
  265.      */
  266.     public function prune(): bool
  267.     {
  268.         return $this->pool instanceof PruneableInterface && $this->pool->prune();
  269.     }
  270.     /**
  271.      * {@inheritdoc}
  272.      */
  273.     public function reset()
  274.     {
  275.         $this->commit();
  276.         $this->knownTagVersions = [];
  277.         $this->pool instanceof ResettableInterface && $this->pool->reset();
  278.         $this->tags instanceof ResettableInterface && $this->tags->reset();
  279.     }
  280.     public function __sleep(): array
  281.     {
  282.         throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
  283.     }
  284.     public function __wakeup()
  285.     {
  286.         throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
  287.     }
  288.     public function __destruct()
  289.     {
  290.         $this->commit();
  291.     }
  292.     private function getTagVersions(array $tagsByKeybool $persistTags): array
  293.     {
  294.         $tagVersions = [];
  295.         $fetchTagVersions $persistTags;
  296.         foreach ($tagsByKey as $tags) {
  297.             $tagVersions += $tags;
  298.             if ($fetchTagVersions) {
  299.                 continue;
  300.             }
  301.             foreach ($tags as $tag => $version) {
  302.                 if ($tagVersions[$tag] !== $version) {
  303.                     $fetchTagVersions true;
  304.                 }
  305.             }
  306.         }
  307.         if (!$tagVersions) {
  308.             return [];
  309.         }
  310.         $now microtime(true);
  311.         $tags = [];
  312.         foreach ($tagVersions as $tag => $version) {
  313.             $tags[$tag.static::TAGS_PREFIX] = $tag;
  314.             $knownTagVersion $this->knownTagVersions[$tag] ?? [0null];
  315.             if ($fetchTagVersions || $now $knownTagVersion[0] || $knownTagVersion[1] !== $version) {
  316.                 // reuse previously fetched tag versions until the expiration
  317.                 $fetchTagVersions true;
  318.             }
  319.         }
  320.         if (!$fetchTagVersions) {
  321.             return $tagVersions;
  322.         }
  323.         $newTags = [];
  324.         $newVersion null;
  325.         $expiration $now $this->knownTagVersionsTtl;
  326.         foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) {
  327.             unset($this->knownTagVersions[$tag $tags[$tag]]); // update FIFO
  328.             if (null !== $tagVersions[$tag] = $version->get()) {
  329.                 $this->knownTagVersions[$tag] = [$expiration$tagVersions[$tag]];
  330.             } elseif ($persistTags) {
  331.                 $newTags[$tag] = $version->set($newVersion ??= random_bytes(6));
  332.                 $tagVersions[$tag] = $newVersion;
  333.                 $this->knownTagVersions[$tag] = [$expiration$newVersion];
  334.             }
  335.         }
  336.         if ($newTags) {
  337.             (self::$saveTags)($this->tags$newTags);
  338.         }
  339.         while ($now > ($this->knownTagVersions[$tag array_key_first($this->knownTagVersions)][0] ?? \INF)) {
  340.             unset($this->knownTagVersions[$tag]);
  341.         }
  342.         return $tagVersions;
  343.     }
  344. }