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

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <[email protected]>
  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 <[email protected]>
  25.  * @author André Rømcke <[email protected]>
  26.  *
  27.  * @internal
  28.  */
  29. abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterfaceTagAwareCacheInterfaceLoggerAwareInterfaceResettableInterface
  30. {
  31.     use AbstractAdapterTrait;
  32.     use ContractsTrait;
  33.     private const TAGS_PREFIX "\0tags\0";
  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 ?? 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] = $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 ?? 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[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::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($value['tags'], $oldTags) as $addedTag) {
  95.                         $value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
  96.                     }
  97.                     foreach (array_diff($oldTags$value['tags']) as $removedTag) {
  98.                         $value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag);
  99.                     }
  100.                     $byLifetime[$ttl][$getId($key)] = $value;
  101.                     $item->metadata $item->newMetadata;
  102.                 }
  103.                 return $byLifetime;
  104.             },
  105.             null,
  106.             CacheItem::class
  107.         );
  108.     }
  109.     /**
  110.      * Persists several cache items immediately.
  111.      *
  112.      * @param array   $values        The values to cache, indexed by their cache identifier
  113.      * @param int     $lifetime      The lifetime of the cached values, 0 for persisting until manual cleaning
  114.      * @param array[] $addTagData    Hash where key is tag id, and array value is list of cache id's to add to tag
  115.      * @param array[] $removeTagData Hash where key is tag id, and array value is list of cache id's to remove to tag
  116.      *
  117.      * @return array The identifiers that failed to be cached or a boolean stating if caching succeeded or not
  118.      */
  119.     abstract protected function doSave(array $valuesint $lifetime, array $addTagData = [], array $removeTagData = []): array;
  120.     /**
  121.      * Removes multiple items from the pool and their corresponding tags.
  122.      *
  123.      * @param array $ids An array of identifiers that should be removed from the pool
  124.      *
  125.      * @return bool
  126.      */
  127.     abstract protected function doDelete(array $ids);
  128.     /**
  129.      * Removes relations between tags and deleted items.
  130.      *
  131.      * @param array $tagData Array of tag => key identifiers that should be removed from the pool
  132.      */
  133.     abstract protected function doDeleteTagRelations(array $tagData): bool;
  134.     /**
  135.      * Invalidates cached items using tags.
  136.      *
  137.      * @param string[] $tagIds An array of tags to invalidate, key is tag and value is tag id
  138.      */
  139.     abstract protected function doInvalidate(array $tagIds): bool;
  140.     /**
  141.      * Delete items and yields the tags they were bound to.
  142.      */
  143.     protected function doDeleteYieldTags(array $ids): iterable
  144.     {
  145.         foreach ($this->doFetch($ids) as $id => $value) {
  146.             yield $id => \is_array($value) && \is_array($value['tags'] ?? null) ? $value['tags'] : [];
  147.         }
  148.         $this->doDelete($ids);
  149.     }
  150.     /**
  151.      * {@inheritdoc}
  152.      */
  153.     public function commit(): bool
  154.     {
  155.         $ok true;
  156.         $byLifetime = (self::$mergeByLifetime)($this->deferred$expiredIds\Closure::fromCallable([$this'getId']), self::TAGS_PREFIX$this->defaultLifetime);
  157.         $retry $this->deferred = [];
  158.         if ($expiredIds) {
  159.             // Tags are not cleaned up in this case, however that is done on invalidateTags().
  160.             try {
  161.                 $this->doDelete($expiredIds);
  162.             } catch (\Exception $e) {
  163.                 $ok false;
  164.                 CacheItem::log($this->logger'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e'cache-adapter' => get_debug_type($this)]);
  165.             }
  166.         }
  167.         foreach ($byLifetime as $lifetime => $values) {
  168.             try {
  169.                 $values $this->extractTagData($values$addTagData$removeTagData);
  170.                 $e $this->doSave($values$lifetime$addTagData$removeTagData);
  171.             } catch (\Exception $e) {
  172.             }
  173.             if (true === $e || [] === $e) {
  174.                 continue;
  175.             }
  176.             if (\is_array($e) || === \count($values)) {
  177.                 foreach (\is_array($e) ? $e array_keys($values) as $id) {
  178.                     $ok false;
  179.                     $v $values[$id];
  180.                     $type get_debug_type($v);
  181.                     $message sprintf('Failed to save key "{key}" of type %s%s'$type$e instanceof \Exception ': '.$e->getMessage() : '.');
  182.                     CacheItem::log($this->logger$message, ['key' => substr($id\strlen($this->namespace)), 'exception' => $e instanceof \Exception $e null'cache-adapter' => get_debug_type($this)]);
  183.                 }
  184.             } else {
  185.                 foreach ($values as $id => $v) {
  186.                     $retry[$lifetime][] = $id;
  187.                 }
  188.             }
  189.         }
  190.         // When bulk-save failed, retry each item individually
  191.         foreach ($retry as $lifetime => $ids) {
  192.             foreach ($ids as $id) {
  193.                 try {
  194.                     $v $byLifetime[$lifetime][$id];
  195.                     $values $this->extractTagData([$id => $v], $addTagData$removeTagData);
  196.                     $e $this->doSave($values$lifetime$addTagData$removeTagData);
  197.                 } catch (\Exception $e) {
  198.                 }
  199.                 if (true === $e || [] === $e) {
  200.                     continue;
  201.                 }
  202.                 $ok false;
  203.                 $type get_debug_type($v);
  204.                 $message sprintf('Failed to save key "{key}" of type %s%s'$type$e instanceof \Exception ': '.$e->getMessage() : '.');
  205.                 CacheItem::log($this->logger$message, ['key' => substr($id\strlen($this->namespace)), 'exception' => $e instanceof \Exception $e null'cache-adapter' => get_debug_type($this)]);
  206.             }
  207.         }
  208.         return $ok;
  209.     }
  210.     /**
  211.      * {@inheritdoc}
  212.      */
  213.     public function deleteItems(array $keys): bool
  214.     {
  215.         if (!$keys) {
  216.             return true;
  217.         }
  218.         $ok true;
  219.         $ids = [];
  220.         $tagData = [];
  221.         foreach ($keys as $key) {
  222.             $ids[$key] = $this->getId($key);
  223.             unset($this->deferred[$key]);
  224.         }
  225.         try {
  226.             foreach ($this->doDeleteYieldTags(array_values($ids)) as $id => $tags) {
  227.                 foreach ($tags as $tag) {
  228.                     $tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
  229.                 }
  230.             }
  231.         } catch (\Exception $e) {
  232.             $ok false;
  233.         }
  234.         try {
  235.             if ((!$tagData || $this->doDeleteTagRelations($tagData)) && $ok) {
  236.                 return true;
  237.             }
  238.         } catch (\Exception $e) {
  239.         }
  240.         // When bulk-delete failed, retry each item individually
  241.         foreach ($ids as $key => $id) {
  242.             try {
  243.                 $e null;
  244.                 if ($this->doDelete([$id])) {
  245.                     continue;
  246.                 }
  247.             } catch (\Exception $e) {
  248.             }
  249.             $message 'Failed to delete key "{key}"'.($e instanceof \Exception ': '.$e->getMessage() : '.');
  250.             CacheItem::log($this->logger$message, ['key' => $key'exception' => $e'cache-adapter' => get_debug_type($this)]);
  251.             $ok false;
  252.         }
  253.         return $ok;
  254.     }
  255.     /**
  256.      * {@inheritdoc}
  257.      */
  258.     public function invalidateTags(array $tags)
  259.     {
  260.         if (empty($tags)) {
  261.             return false;
  262.         }
  263.         $tagIds = [];
  264.         foreach (array_unique($tags) as $tag) {
  265.             $tagIds[] = $this->getId(self::TAGS_PREFIX.$tag);
  266.         }
  267.         try {
  268.             if ($this->doInvalidate($tagIds)) {
  269.                 return true;
  270.             }
  271.         } catch (\Exception $e) {
  272.             CacheItem::log($this->logger'Failed to invalidate tags: '.$e->getMessage(), ['exception' => $e'cache-adapter' => get_debug_type($this)]);
  273.         }
  274.         return false;
  275.     }
  276.     /**
  277.      * Extracts tags operation data from $values set in mergeByLifetime, and returns values without it.
  278.      */
  279.     private function extractTagData(array $values, ?array &$addTagData, ?array &$removeTagData): array
  280.     {
  281.         $addTagData $removeTagData = [];
  282.         foreach ($values as $id => $value) {
  283.             foreach ($value['tag-operations']['add'] as $tag => $tagId) {
  284.                 $addTagData[$tagId][] = $id;
  285.             }
  286.             foreach ($value['tag-operations']['remove'] as $tag => $tagId) {
  287.                 $removeTagData[$tagId][] = $id;
  288.             }
  289.             unset($values[$id]['tag-operations']);
  290.         }
  291.         return $values;
  292.     }
  293. }