vendor/symfony/cache/Adapter/AbstractTagAwareAdapter.php line 186

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\Log\LoggerAwareInterface;
  12. use Symfony\Component\Cache\CacheItem;
  13. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  14. use Symfony\Component\Cache\ResettableInterface;
  15. use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
  16. use Symfony\Component\Cache\Traits\ContractsTrait;
  17. use Symfony\Contracts\Cache\TagAwareCacheInterface;
  18. /**
  19.  * Abstract for native TagAware adapters.
  20.  *
  21.  * To keep info on tags, the tags are both serialized as part of cache value and provided as tag ids
  22.  * to Adapters on operations when needed for storage to doSave(), doDelete() & doInvalidate().
  23.  *
  24.  * @author Nicolas Grekas <p@tchwork.com>
  25.  * @author André Rømcke <andre.romcke+symfony@gmail.com>
  26.  *
  27.  * @internal
  28.  */
  29. abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterfaceTagAwareCacheInterfaceLoggerAwareInterfaceResettableInterface
  30. {
  31.     use AbstractAdapterTrait;
  32.     use ContractsTrait;
  33.     private const TAGS_PREFIX "\1tags\1";
  34.     protected function __construct(string $namespace ''int $defaultLifetime 0)
  35.     {
  36.         $this->namespace '' === $namespace '' CacheItem::validateKey($namespace).':';
  37.         $this->defaultLifetime $defaultLifetime;
  38.         if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength 24) {
  39.             throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").'$this->maxIdLength 24\strlen($namespace), $namespace));
  40.         }
  41.         self::$createCacheItem ??= \Closure::bind(
  42.             static function ($key$value$isHit) {
  43.                 $item = new CacheItem();
  44.                 $item->key $key;
  45.                 $item->isTaggable true;
  46.                 // If structure does not match what we expect return item as is (no value and not a hit)
  47.                 if (!\is_array($value) || !\array_key_exists('value'$value)) {
  48.                     return $item;
  49.                 }
  50.                 $item->isHit $isHit;
  51.                 // Extract value, tags and meta data from the cache value
  52.                 $item->value $value['value'];
  53.                 $item->metadata[CacheItem::METADATA_TAGS] = isset($value['tags']) ? array_combine($value['tags'], $value['tags']) : [];
  54.                 if (isset($value['meta'])) {
  55.                     // For compactness these values are packed, & expiry is offset to reduce size
  56.                     $v unpack('Ve/Nc'$value['meta']);
  57.                     $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
  58.                     $item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
  59.                 }
  60.                 return $item;
  61.             },
  62.             null,
  63.             CacheItem::class
  64.         );
  65.         self::$mergeByLifetime ??= \Closure::bind(
  66.             static function ($deferred, &$expiredIds$getId$tagPrefix$defaultLifetime) {
  67.                 $byLifetime = [];
  68.                 $now microtime(true);
  69.                 $expiredIds = [];
  70.                 foreach ($deferred as $key => $item) {
  71.                     $key = (string) $key;
  72.                     if (null === $item->expiry) {
  73.                         $ttl $defaultLifetime $defaultLifetime 0;
  74.                     } elseif (!$item->expiry) {
  75.                         $ttl 0;
  76.                     } elseif (>= $ttl = (int) (0.1 $item->expiry $now)) {
  77.                         $expiredIds[] = $getId($key);
  78.                         continue;
  79.                     }
  80.                     // Store Value and Tags on the cache value
  81.                     if (isset(($metadata $item->newMetadata)[CacheItem::METADATA_TAGS])) {
  82.                         $value = ['value' => $item->value'tags' => $metadata[CacheItem::METADATA_TAGS]];
  83.                         unset($metadata[CacheItem::METADATA_TAGS]);
  84.                     } else {
  85.                         $value = ['value' => $item->value'tags' => []];
  86.                     }
  87.                     if ($metadata) {
  88.                         // For compactness, expiry and creation duration are packed, using magic numbers as separators
  89.                         $value['meta'] = pack('VN', (int) (0.1 $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET), $metadata[CacheItem::METADATA_CTIME]);
  90.                     }
  91.                     // Extract tag changes, these should be removed from values in doSave()
  92.                     $value['tag-operations'] = ['add' => [], 'remove' => []];
  93.                     $oldTags $item->metadata[CacheItem::METADATA_TAGS] ?? [];
  94.                     foreach (array_diff_key($value['tags'], $oldTags) as $addedTag) {
  95.                         $value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
  96.                     }
  97.                     foreach (array_diff_key($oldTags$value['tags']) as $removedTag) {
  98.                         $value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag);
  99.                     }
  100.                     $value['tags'] = array_keys($value['tags']);
  101.                     $byLifetime[$ttl][$getId($key)] = $value;
  102.                     $item->metadata $item->newMetadata;
  103.                 }
  104.                 return $byLifetime;
  105.             },
  106.             null,
  107.             CacheItem::class
  108.         );
  109.     }
  110.     /**
  111.      * Persists several cache items immediately.
  112.      *
  113.      * @param array   $values        The values to cache, indexed by their cache identifier
  114.      * @param int     $lifetime      The lifetime of the cached values, 0 for persisting until manual cleaning
  115.      * @param array[] $addTagData    Hash where key is tag id, and array value is list of cache id's to add to tag
  116.      * @param array[] $removeTagData Hash where key is tag id, and array value is list of cache id's to remove to tag
  117.      *
  118.      * @return array The identifiers that failed to be cached or a boolean stating if caching succeeded or not
  119.      */
  120.     abstract protected function doSave(array $valuesint $lifetime, array $addTagData = [], array $removeTagData = []): array;
  121.     /**
  122.      * Removes multiple items from the pool and their corresponding tags.
  123.      *
  124.      * @param array $ids An array of identifiers that should be removed from the pool
  125.      */
  126.     abstract protected function doDelete(array $ids): bool;
  127.     /**
  128.      * Removes relations between tags and deleted items.
  129.      *
  130.      * @param array $tagData Array of tag => key identifiers that should be removed from the pool
  131.      */
  132.     abstract protected function doDeleteTagRelations(array $tagData): bool;
  133.     /**
  134.      * Invalidates cached items using tags.
  135.      *
  136.      * @param string[] $tagIds An array of tags to invalidate, key is tag and value is tag id
  137.      */
  138.     abstract protected function doInvalidate(array $tagIds): bool;
  139.     /**
  140.      * Delete items and yields the tags they were bound to.
  141.      */
  142.     protected function doDeleteYieldTags(array $ids): iterable
  143.     {
  144.         foreach ($this->doFetch($ids) as $id => $value) {
  145.             yield $id => \is_array($value) && \is_array($value['tags'] ?? null) ? $value['tags'] : [];
  146.         }
  147.         $this->doDelete($ids);
  148.     }
  149.     public function commit(): bool
  150.     {
  151.         $ok true;
  152.         $byLifetime = (self::$mergeByLifetime)($this->deferred$expiredIds$this->getId(...), self::TAGS_PREFIX$this->defaultLifetime);
  153.         $retry $this->deferred = [];
  154.         if ($expiredIds) {
  155.             // Tags are not cleaned up in this case, however that is done on invalidateTags().
  156.             try {
  157.                 $this->doDelete($expiredIds);
  158.             } catch (\Exception $e) {
  159.                 $ok false;
  160.                 CacheItem::log($this->logger'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e'cache-adapter' => get_debug_type($this)]);
  161.             }
  162.         }
  163.         foreach ($byLifetime as $lifetime => $values) {
  164.             try {
  165.                 $values $this->extractTagData($values$addTagData$removeTagData);
  166.                 $e $this->doSave($values$lifetime$addTagData$removeTagData);
  167.             } catch (\Exception $e) {
  168.             }
  169.             if (true === $e || [] === $e) {
  170.                 continue;
  171.             }
  172.             if (\is_array($e) || === \count($values)) {
  173.                 foreach (\is_array($e) ? $e array_keys($values) as $id) {
  174.                     $ok false;
  175.                     $v $values[$id];
  176.                     $type get_debug_type($v);
  177.                     $message sprintf('Failed to save key "{key}" of type %s%s'$type$e instanceof \Exception ': '.$e->getMessage() : '.');
  178.                     CacheItem::log($this->logger$message, ['key' => substr($id\strlen($this->namespace)), 'exception' => $e instanceof \Exception $e null'cache-adapter' => get_debug_type($this)]);
  179.                 }
  180.             } else {
  181.                 foreach ($values as $id => $v) {
  182.                     $retry[$lifetime][] = $id;
  183.                 }
  184.             }
  185.         }
  186.         // When bulk-save failed, retry each item individually
  187.         foreach ($retry as $lifetime => $ids) {
  188.             foreach ($ids as $id) {
  189.                 try {
  190.                     $v $byLifetime[$lifetime][$id];
  191.                     $values $this->extractTagData([$id => $v], $addTagData$removeTagData);
  192.                     $e $this->doSave($values$lifetime$addTagData$removeTagData);
  193.                 } catch (\Exception $e) {
  194.                 }
  195.                 if (true === $e || [] === $e) {
  196.                     continue;
  197.                 }
  198.                 $ok false;
  199.                 $type get_debug_type($v);
  200.                 $message sprintf('Failed to save key "{key}" of type %s%s'$type$e instanceof \Exception ': '.$e->getMessage() : '.');
  201.                 CacheItem::log($this->logger$message, ['key' => substr($id\strlen($this->namespace)), 'exception' => $e instanceof \Exception $e null'cache-adapter' => get_debug_type($this)]);
  202.             }
  203.         }
  204.         return $ok;
  205.     }
  206.     public function deleteItems(array $keys): bool
  207.     {
  208.         if (!$keys) {
  209.             return true;
  210.         }
  211.         $ok true;
  212.         $ids = [];
  213.         $tagData = [];
  214.         foreach ($keys as $key) {
  215.             $ids[$key] = $this->getId($key);
  216.             unset($this->deferred[$key]);
  217.         }
  218.         try {
  219.             foreach ($this->doDeleteYieldTags(array_values($ids)) as $id => $tags) {
  220.                 foreach ($tags as $tag) {
  221.                     $tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
  222.                 }
  223.             }
  224.         } catch (\Exception) {
  225.             $ok false;
  226.         }
  227.         try {
  228.             if ((!$tagData || $this->doDeleteTagRelations($tagData)) && $ok) {
  229.                 return true;
  230.             }
  231.         } catch (\Exception) {
  232.         }
  233.         // When bulk-delete failed, retry each item individually
  234.         foreach ($ids as $key => $id) {
  235.             try {
  236.                 $e null;
  237.                 if ($this->doDelete([$id])) {
  238.                     continue;
  239.                 }
  240.             } catch (\Exception $e) {
  241.             }
  242.             $message 'Failed to delete key "{key}"'.($e instanceof \Exception ': '.$e->getMessage() : '.');
  243.             CacheItem::log($this->logger$message, ['key' => $key'exception' => $e'cache-adapter' => get_debug_type($this)]);
  244.             $ok false;
  245.         }
  246.         return $ok;
  247.     }
  248.     public function invalidateTags(array $tags): bool
  249.     {
  250.         if (!$tags) {
  251.             return false;
  252.         }
  253.         $tagIds = [];
  254.         foreach (array_unique($tags) as $tag) {
  255.             $tagIds[] = $this->getId(self::TAGS_PREFIX.$tag);
  256.         }
  257.         try {
  258.             if ($this->doInvalidate($tagIds)) {
  259.                 return true;
  260.             }
  261.         } catch (\Exception $e) {
  262.             CacheItem::log($this->logger'Failed to invalidate tags: '.$e->getMessage(), ['exception' => $e'cache-adapter' => get_debug_type($this)]);
  263.         }
  264.         return false;
  265.     }
  266.     /**
  267.      * Extracts tags operation data from $values set in mergeByLifetime, and returns values without it.
  268.      */
  269.     private function extractTagData(array $values, ?array &$addTagData, ?array &$removeTagData): array
  270.     {
  271.         $addTagData $removeTagData = [];
  272.         foreach ($values as $id => $value) {
  273.             foreach ($value['tag-operations']['add'] as $tag => $tagId) {
  274.                 $addTagData[$tagId][] = $id;
  275.             }
  276.             foreach ($value['tag-operations']['remove'] as $tag => $tagId) {
  277.                 $removeTagData[$tagId][] = $id;
  278.             }
  279.             unset($values[$id]['tag-operations']);
  280.         }
  281.         return $values;
  282.     }
  283. }