vendor/pimcore/pimcore/lib/Cache/Core/CoreCacheHandler.php line 328

Open in your IDE?
  1. <?php
  2. /**
  3.  * Pimcore
  4.  *
  5.  * This source file is available under two different licenses:
  6.  * - GNU General Public License version 3 (GPLv3)
  7.  * - Pimcore Commercial License (PCL)
  8.  * Full copyright and license information is available in
  9.  * LICENSE.md which is distributed with this source code.
  10.  *
  11.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12.  *  @license    http://www.pimcore.org/license     GPLv3 and PCL
  13.  */
  14. namespace Pimcore\Cache\Core;
  15. use DeepCopy\TypeMatcher\TypeMatcher;
  16. use Pimcore\Event\CoreCacheEvents;
  17. use Pimcore\Model\Document\Hardlink\Wrapper\WrapperInterface;
  18. use Pimcore\Model\Element\ElementDumpStateInterface;
  19. use Pimcore\Model\Element\ElementInterface;
  20. use Pimcore\Model\Element\Service;
  21. use Pimcore\Model\Version\SetDumpStateFilter;
  22. use Psr\Log\LoggerAwareInterface;
  23. use Psr\Log\LoggerAwareTrait;
  24. use Psr\Log\LoggerInterface;
  25. use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
  26. use Symfony\Component\Cache\CacheItem;
  27. use Symfony\Contracts\EventDispatcher\Event;
  28. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  29. /**
  30.  * Core pimcore cache handler with logic handling deferred save on shutdown (specialized for internal pimcore use). This
  31.  * explicitely does not expose a PSR-6 API but is intended for internal use from Pimcore\Cache or directly. Actual
  32.  * cache calls are forwarded to a PSR-6 cache implementation though.
  33.  *
  34.  * Use Pimcore\Cache static interface, do not use this handler directly
  35.  *
  36.  * @internal
  37.  */
  38. class CoreCacheHandler implements LoggerAwareInterface
  39. {
  40.     use LoggerAwareTrait;
  41.     /**
  42.      * @var EventDispatcherInterface
  43.      */
  44.     protected $dispatcher;
  45.     /**
  46.      * @var TagAwareAdapterInterface
  47.      */
  48.     protected $pool;
  49.     /**
  50.      * @var WriteLock
  51.      */
  52.     protected $writeLock;
  53.     /**
  54.      * Actually write/load to/from cache?
  55.      *
  56.      * @var bool
  57.      */
  58.     protected $enabled true;
  59.     /**
  60.      * Is the cache handled in CLI mode?
  61.      *
  62.      * @var bool
  63.      */
  64.     protected $handleCli false;
  65.     /**
  66.      * Contains the items which should be written to the cache on shutdown
  67.      *
  68.      * @var CacheQueueItem[]
  69.      */
  70.     protected $saveQueue = [];
  71.     /**
  72.      * Tags which were already cleared
  73.      *
  74.      * @var array
  75.      */
  76.     protected $clearedTags = [];
  77.     /**
  78.      * Items having one of the tags in this list are not saved
  79.      *
  80.      * @var array
  81.      */
  82.     protected $tagsIgnoredOnSave = [];
  83.     /**
  84.      * Items having one of the tags in this list are not cleared when calling clearTags
  85.      *
  86.      * @var array
  87.      */
  88.     protected $tagsIgnoredOnClear = [];
  89.     /**
  90.      * Items having tags which are in this array are cleared on shutdown. This is especially for the output-cache.
  91.      *
  92.      * @var array
  93.      */
  94.     protected $tagsClearedOnShutdown = [];
  95.     /**
  96.      * State variable which is set to true after the cache was cleared - prevent new items being
  97.      * written to cache after a clear.
  98.      *
  99.      * @var bool
  100.      */
  101.     protected $cacheCleared false;
  102.     /**
  103.      * Tags in this list are shifted to the clearTagsOnShutdown list when scheduled via clearTags. See comment on normalizeClearTags
  104.      * method why this exists.
  105.      *
  106.      * @var array
  107.      */
  108.     protected $shutdownTags = ['output'];
  109.     /**
  110.      * If set to true items are directly written into the cache, and do not get into the queue
  111.      *
  112.      * @var bool
  113.      */
  114.     protected $forceImmediateWrite false;
  115.     /**
  116.      * How many items should stored to the cache within one process
  117.      *
  118.      * @var int
  119.      */
  120.     protected $maxWriteToCacheItems 50;
  121.     /**
  122.      * @var bool
  123.      */
  124.     protected $writeInProgress false;
  125.     /**
  126.      * @var \Closure
  127.      */
  128.     protected $emptyCacheItemClosure;
  129.     /**
  130.      * @param TagAwareAdapterInterface $adapter
  131.      * @param WriteLock $writeLock
  132.      * @param EventDispatcherInterface $dispatcher
  133.      */
  134.     public function __construct(TagAwareAdapterInterface $adapterWriteLock $writeLockEventDispatcherInterface $dispatcher)
  135.     {
  136.         $this->pool $adapter;
  137.         $this->dispatcher $dispatcher;
  138.         $this->writeLock $writeLock;
  139.     }
  140.     /**
  141.      * @internal
  142.      *
  143.      * @param TagAwareAdapterInterface $pool
  144.      */
  145.     public function setPool(TagAwareAdapterInterface $pool): void
  146.     {
  147.         $this->pool $pool;
  148.     }
  149.     /**
  150.      * @return WriteLock
  151.      */
  152.     public function getWriteLock()
  153.     {
  154.         return $this->writeLock;
  155.     }
  156.     /**
  157.      * @codeCoverageIgnore
  158.      *
  159.      * @return LoggerInterface
  160.      */
  161.     public function getLogger()
  162.     {
  163.         return $this->logger;
  164.     }
  165.     /**
  166.      * {@inheritdoc}
  167.      */
  168.     public function enable()
  169.     {
  170.         $this->enabled true;
  171.         $this->dispatchStatusEvent();
  172.         return $this;
  173.     }
  174.     /**
  175.      * {@inheritdoc}
  176.      */
  177.     public function disable()
  178.     {
  179.         $this->enabled false;
  180.         $this->dispatchStatusEvent();
  181.         return $this;
  182.     }
  183.     /**
  184.      * @return bool
  185.      */
  186.     public function isEnabled()
  187.     {
  188.         return $this->enabled;
  189.     }
  190.     protected function dispatchStatusEvent()
  191.     {
  192.         $this->dispatcher->dispatch(new Event(),
  193.             $this->isEnabled()
  194.                 ? CoreCacheEvents::ENABLE
  195.                 CoreCacheEvents::DISABLE
  196.         );
  197.     }
  198.     /**
  199.      * @codeCoverageIgnore
  200.      *
  201.      * @return bool
  202.      */
  203.     public function getHandleCli()
  204.     {
  205.         return $this->handleCli;
  206.     }
  207.     /**
  208.      * @codeCoverageIgnore
  209.      *
  210.      * @param bool $handleCli
  211.      *
  212.      * @return $this
  213.      */
  214.     public function setHandleCli($handleCli)
  215.     {
  216.         $this->handleCli = (bool)$handleCli;
  217.         return $this;
  218.     }
  219.     /**
  220.      * @codeCoverageIgnore
  221.      *
  222.      * @return bool
  223.      */
  224.     public function getForceImmediateWrite()
  225.     {
  226.         return $this->forceImmediateWrite;
  227.     }
  228.     /**
  229.      * @codeCoverageIgnore
  230.      *
  231.      * @param bool $forceImmediateWrite
  232.      *
  233.      * @return $this
  234.      */
  235.     public function setForceImmediateWrite($forceImmediateWrite)
  236.     {
  237.         $this->forceImmediateWrite = (bool)$forceImmediateWrite;
  238.         return $this;
  239.     }
  240.     /**
  241.      * @param int $maxWriteToCacheItems
  242.      *
  243.      * @return $this
  244.      */
  245.     public function setMaxWriteToCacheItems($maxWriteToCacheItems)
  246.     {
  247.         $this->maxWriteToCacheItems = (int)$maxWriteToCacheItems;
  248.         return $this;
  249.     }
  250.     /**
  251.      * Load data from cache (retrieves data from cache item)
  252.      *
  253.      * @param string $key
  254.      *
  255.      * @return mixed
  256.      */
  257.     public function load($key)
  258.     {
  259.         if (!$this->enabled) {
  260.             $this->logger->debug('Not loading object {key} from cache (deactivated)', ['key' => $key]);
  261.             return false;
  262.         }
  263.         $item $this->getItem($key);
  264.         if ($item->isHit()) {
  265.             $data $item->get();
  266.             if (is_object($data)) {
  267.                 $data->____pimcore_cache_item__ $key// TODO where is this used?
  268.             }
  269.             return $data;
  270.         }
  271.         return false;
  272.     }
  273.     /**
  274.      * Get PSR-6 cache item
  275.      *
  276.      * @param string $key
  277.      *
  278.      * @return CacheItem
  279.      */
  280.     public function getItem($key)
  281.     {
  282.         $item $this->pool->getItem($key);
  283.         if ($item->isHit()) {
  284.             $this->logger->debug('Successfully got data for key {key} from cache', ['key' => $key]);
  285.         } else {
  286.             $this->logger->debug('Key {key} doesn\'t exist in cache', ['key' => $key]);
  287.         }
  288.         return $item;
  289.     }
  290.     /**
  291.      * Save data to cache
  292.      *
  293.      * @param string $key
  294.      * @param mixed $data
  295.      * @param array $tags
  296.      * @param int|\DateInterval|null $lifetime
  297.      * @param int|null $priority
  298.      * @param bool $force
  299.      *
  300.      * @return bool
  301.      */
  302.     public function save($key$data, array $tags = [], $lifetime null$priority 0$force false)
  303.     {
  304.         if ($this->writeInProgress) {
  305.             return false;
  306.         }
  307.         CacheItem::validateKey($key);
  308.         if (!$this->enabled) {
  309.             $this->logger->debug('Not saving object {key} to cache (deactivated)', ['key' => $key]);
  310.             return false;
  311.         }
  312.         if ($this->isCli()) {
  313.             if (!$this->handleCli && !$force) {
  314.                 $this->logger->debug(
  315.                     'Not saving {key} to cache as process is running in CLI mode (pass force to override or set handleCli to true)',
  316.                     ['key' => $key]
  317.                 );
  318.                 return false;
  319.             }
  320.         }
  321.         if ($force || $this->forceImmediateWrite) {
  322.             $data $this->prepareCacheData($data);
  323.             if (null === $data) {
  324.                 // logging is done in prepare method if item could not be created
  325.                 return false;
  326.             }
  327.             // add cache tags to item
  328.             $tags $this->prepareCacheTags($key$data$tags);
  329.             if (null === $tags) {
  330.                 return false;
  331.             }
  332.             return $this->storeCacheData($key$data$tags$lifetime$force);
  333.         } else {
  334.             $cacheQueueItem = new CacheQueueItem($key$data$tags$lifetime$priority$force);
  335.             return $this->addToSaveQueue($cacheQueueItem);
  336.         }
  337.     }
  338.     /**
  339.      * Add item to save queue, respecting maxWriteToCacheItems setting
  340.      *
  341.      * @param CacheQueueItem $item
  342.      *
  343.      * @return bool
  344.      */
  345.     protected function addToSaveQueue(CacheQueueItem $item)
  346.     {
  347.         $data $this->prepareCacheData($item->getData());
  348.         if ($data) {
  349.             $this->saveQueue[$item->getKey()] = $item;
  350.             if (count($this->saveQueue) > ($this->maxWriteToCacheItems*3)) {
  351.                 $this->cleanupQueue();
  352.             }
  353.             return true;
  354.         }
  355.         return false;
  356.     }
  357.     /**
  358.      * @internal
  359.      */
  360.     public function cleanupQueue(): void
  361.     {
  362.         // order by priority
  363.         uasort($this->saveQueue, function (CacheQueueItem $aCacheQueueItem $b) {
  364.             return $b->getPriority() <=> $a->getPriority();
  365.         });
  366.         // remove overrun
  367.         array_splice($this->saveQueue$this->maxWriteToCacheItems);
  368.     }
  369.     /**
  370.      * Prepare data for cache item and handle items we don't want to save (e.g. hardlinks)
  371.      *
  372.      * @param mixed $data
  373.      *
  374.      * @return mixed
  375.      */
  376.     protected function prepareCacheData($data)
  377.     {
  378.         // do not cache hardlink-wrappers
  379.         if ($data instanceof WrapperInterface) {
  380.             return null;
  381.         }
  382.         // clean up and prepare models
  383.         if ($data instanceof ElementInterface) {
  384.             // check for corrupt data
  385.             if (!$data->getId()) {
  386.                 return null;
  387.             }
  388.         }
  389.         return $data;
  390.     }
  391.     /**
  392.      * Create tags for cache item - do this as late as possible as this is potentially expensive (nested items, dependencies)
  393.      *
  394.      * @param string $key
  395.      * @param mixed $data
  396.      * @param array $tags
  397.      *
  398.      * @return null|string[]
  399.      */
  400.     protected function prepareCacheTags(string $key$data, array $tags = [])
  401.     {
  402.         // clean up and prepare models
  403.         if ($data instanceof ElementInterface) {
  404.             // get tags for this element
  405.             $tags $data->getCacheTags($tags);
  406.             $this->logger->debug(
  407.                 'Prepared {class} {id} for data cache',
  408.                 [
  409.                     'class' => get_class($data),
  410.                     'id' => $data->getId(),
  411.                     'tags' => $tags,
  412.                 ]
  413.             );
  414.         }
  415.         // array_values() because the tags from \Element_Interface and some others are associative eg. array("object_123" => "object_123")
  416.         $tags array_values($tags);
  417.         $tags array_unique($tags);
  418.         // check if any of our tags is in cleared tags or tags ignored on save lists
  419.         foreach ($tags as $tag) {
  420.             if (isset($this->clearedTags[$tag])) {
  421.                 $this->logger->debug('Aborted caching for key {key} because tag {tag} is in the cleared tags list', [
  422.                     'key' => $key,
  423.                     'tag' => $tag,
  424.                 ]);
  425.                 return null;
  426.             }
  427.             if (in_array($tag$this->tagsIgnoredOnSave)) {
  428.                 $this->logger->debug('Aborted caching for key {key} because tag {tag} is in the ignored tags on save list', [
  429.                     'key' => $key,
  430.                     'tag' => $tag,
  431.                     'tags' => $tags,
  432.                     'tagsIgnoredOnSave' => $this->tagsIgnoredOnSave,
  433.                 ]);
  434.                 return null;
  435.             }
  436.         }
  437.         return $tags;
  438.     }
  439.     /**
  440.      * @param string $key
  441.      * @param mixed $data
  442.      * @param array $tags
  443.      * @param int|\DateInterval|null $lifetime
  444.      * @param bool $force
  445.      *
  446.      * @return bool
  447.      */
  448.     protected function storeCacheData(string $key$data, array $tags = [], $lifetime null$force false)
  449.     {
  450.         if ($this->writeInProgress) {
  451.             return false;
  452.         }
  453.         if (!$this->enabled) {
  454.             // TODO return true here as the noop (not storing anything) is basically successful?
  455.             return false;
  456.         }
  457.         // don't put anything into the cache, when cache is cleared
  458.         if ($this->cacheCleared && !$force) {
  459.             return false;
  460.         }
  461.         $this->writeInProgress true;
  462.         if ($data instanceof ElementInterface) {
  463.             // fetch a fresh copy
  464.             $type Service::getElementType($data);
  465.             $data Service::getElementById($type$data->getId(), ['force' => true]);
  466.             if (!$data->__isBasedOnLatestData()) {
  467.                 $this->logger->warning('Not saving {key} to cache as element is not based on latest data', [
  468.                     'key' => $key,
  469.                 ]);
  470.                 $this->writeInProgress false;
  471.                 return false;
  472.             }
  473.             // dump state is used to trigger a full serialized dump in __sleep eg. in Document, AbstractObject
  474.             $data->setInDumpState(false);
  475.             $context = [
  476.                 'source' => __METHOD__,
  477.                 'conversion' => false,
  478.             ];
  479.             $copier Service::getDeepCopyInstance($data$context);
  480.             $copier->addFilter(new SetDumpStateFilter(false), new \DeepCopy\Matcher\PropertyMatcher(ElementDumpStateInterface::class, ElementDumpStateInterface::DUMP_STATE_PROPERTY_NAME));
  481.             $copier->addTypeFilter(
  482.                 new \DeepCopy\TypeFilter\ReplaceFilter(
  483.                     function ($currentValue) {
  484.                         if ($currentValue instanceof CacheMarshallerInterface) {
  485.                             $marshalledValue $currentValue->marshalForCache();
  486.                             return $marshalledValue;
  487.                         }
  488.                         return $currentValue;
  489.                     }
  490.                 ),
  491.                 new TypeMatcher(CacheMarshallerInterface::class)
  492.             );
  493.             $data $copier->copy($data);
  494.         }
  495.         $item $this->pool->getItem($key);
  496.         $item->set($data);
  497.         $item->expiresAfter($lifetime);
  498.         $item->tag($tags);
  499.         $item->tag($key);
  500.         $result $this->pool->save($item);
  501.         if ($result) {
  502.             $this->logger->debug('Added entry {key} to cache', ['key' => $item->getKey()]);
  503.         } else {
  504.             try {
  505.                 $itemData $item->get();
  506.                 if (!is_scalar($itemData)) {
  507.                     $itemData serialize($itemData);
  508.                 }
  509.                 $itemSizeText formatBytes(mb_strlen((string) $itemData));
  510.             } catch (\Throwable $e) {
  511.                 $itemSizeText 'unknown';
  512.             }
  513.             $this->logger->error(
  514.                 'Failed to add entry {key} to cache. Item size was {itemSize}',
  515.                 [
  516.                     'key' => $item->getKey(),
  517.                     'itemSize' => $itemSizeText,
  518.                 ]
  519.             );
  520.         }
  521.         $this->writeInProgress false;
  522.         return $result;
  523.     }
  524.     /**
  525.      * Remove a cache item
  526.      *
  527.      * @param string $key
  528.      *
  529.      * @return bool
  530.      */
  531.     public function remove($key)
  532.     {
  533.         CacheItem::validateKey($key);
  534.         $this->writeLock->lock();
  535.         return $this->pool->deleteItem($key);
  536.     }
  537.     /**
  538.      * Empty the cache
  539.      *
  540.      * @return bool
  541.      */
  542.     public function clearAll()
  543.     {
  544.         $this->writeLock->lock();
  545.         $this->logger->info('Clearing the whole cache');
  546.         $result $this->pool->clear();
  547.         // immediately acquire the write lock again (force), because the lock is in the cache too
  548.         $this->writeLock->lock(true);
  549.         // set state to cache cleared - prevents new items being written to cache
  550.         $this->cacheCleared true;
  551.         return $result;
  552.     }
  553.     /**
  554.      * @param string $tag
  555.      *
  556.      * @return bool
  557.      */
  558.     public function clearTag($tag)
  559.     {
  560.         return $this->clearTags([$tag]);
  561.     }
  562.     /**
  563.      * @param string[] $tags
  564.      *
  565.      * @return bool
  566.      */
  567.     public function clearTags(array $tags): bool
  568.     {
  569.         $this->writeLock->lock();
  570.         $originalTags $tags;
  571.         $this->logger->debug(
  572.             'Clearing cache tags',
  573.             ['tags' => $tags]
  574.         );
  575.         $tags $this->normalizeClearTags($tags);
  576.         if (count($tags) > 0) {
  577.             $result $this->pool->invalidateTags($tags);
  578.             $this->addClearedTags($tags);
  579.             return $result;
  580.         }
  581.         $this->logger->warning(
  582.             'Could not clear tags as tag list is empty after normalization',
  583.             [
  584.                 'tags' => $tags,
  585.                 'originalTags' => $originalTags,
  586.             ]
  587.         );
  588.         return false;
  589.     }
  590.     /**
  591.      * Clears all tags stored in tagsClearedOnShutdown, this function is executed during Pimcore shutdown
  592.      *
  593.      * @return bool
  594.      */
  595.     public function clearTagsOnShutdown()
  596.     {
  597.         if (empty($this->tagsClearedOnShutdown)) {
  598.             return true;
  599.         }
  600.         $this->logger->debug('Clearing shutdown cache tags', ['tags' => $this->tagsClearedOnShutdown]);
  601.         $result $this->pool->invalidateTags($this->tagsClearedOnShutdown);
  602.         $this->addClearedTags($this->tagsClearedOnShutdown);
  603.         $this->tagsClearedOnShutdown = [];
  604.         return $result;
  605.     }
  606.     /**
  607.      * Normalize (unique) clear tags and shift special tags to shutdown (e.g. output)
  608.      *
  609.      * @param array $tags
  610.      *
  611.      * @return array
  612.      */
  613.     protected function normalizeClearTags(array $tags)
  614.     {
  615.         $blacklist $this->tagsIgnoredOnClear;
  616.         // Shutdown tags are special tags being shifted to shutdown when scheduled to clear via clearTags. Explanation for
  617.         // the "output" tag:
  618.         // check for the tag output, because items with this tags are only cleared after the process is finished
  619.         // the reason is that eg. long running importers will clean the output-cache on every save/update, that's not necessary,
  620.         // only cleaning the output-cache on shutdown should be enough
  621.         foreach ($this->shutdownTags as $shutdownTag) {
  622.             if (in_array($shutdownTag$tags)) {
  623.                 $this->addTagClearedOnShutdown($shutdownTag);
  624.                 $blacklist[] = $shutdownTag;
  625.             }
  626.         }
  627.         // ensure that every tag is unique
  628.         $tags array_unique($tags);
  629.         // don't clear tags in ignore array
  630.         $tags array_filter($tags, function ($tag) use ($blacklist) {
  631.             return !in_array($tag$blacklist);
  632.         });
  633.         return $tags;
  634.     }
  635.     /**
  636.      * Add tag to list of cleared tags (internal use only)
  637.      *
  638.      * @param string|array $tags
  639.      *
  640.      * @return $this
  641.      */
  642.     protected function addClearedTags($tags)
  643.     {
  644.         if (!is_array($tags)) {
  645.             $tags = [$tags];
  646.         }
  647.         foreach ($tags as $tag) {
  648.             $this->clearedTags[$tag] = true;
  649.         }
  650.         return $this;
  651.     }
  652.     /**
  653.      * Adds a tag to the shutdown queue, see clearTagsOnShutdown
  654.      *
  655.      * @internal
  656.      *
  657.      * @param string $tag
  658.      *
  659.      * @return $this
  660.      */
  661.     public function addTagClearedOnShutdown($tag)
  662.     {
  663.         $this->writeLock->lock();
  664.         $this->tagsClearedOnShutdown[] = $tag;
  665.         $this->tagsClearedOnShutdown array_unique($this->tagsClearedOnShutdown);
  666.         return $this;
  667.     }
  668.     /**
  669.      * @internal
  670.      *
  671.      * @param string $tag
  672.      *
  673.      * @return $this
  674.      */
  675.     public function addTagIgnoredOnSave($tag)
  676.     {
  677.         $this->tagsIgnoredOnSave[] = $tag;
  678.         $this->tagsIgnoredOnSave array_unique($this->tagsIgnoredOnSave);
  679.         return $this;
  680.     }
  681.     /**
  682.      * @internal
  683.      *
  684.      * @param string $tag
  685.      *
  686.      * @return $this
  687.      */
  688.     public function removeTagIgnoredOnSave($tag)
  689.     {
  690.         $this->tagsIgnoredOnSave array_filter($this->tagsIgnoredOnSave, function ($t) use ($tag) {
  691.             return $t !== $tag;
  692.         });
  693.         return $this;
  694.     }
  695.     /**
  696.      * @internal
  697.      *
  698.      * @param string $tag
  699.      *
  700.      * @return $this
  701.      */
  702.     public function addTagIgnoredOnClear($tag)
  703.     {
  704.         $this->tagsIgnoredOnClear[] = $tag;
  705.         $this->tagsIgnoredOnClear array_unique($this->tagsIgnoredOnClear);
  706.         return $this;
  707.     }
  708.     /**
  709.      * @internal
  710.      *
  711.      * @param string $tag
  712.      *
  713.      * @return $this
  714.      */
  715.     public function removeTagIgnoredOnClear($tag)
  716.     {
  717.         $this->tagsIgnoredOnClear array_filter($this->tagsIgnoredOnClear, function ($t) use ($tag) {
  718.             return $t !== $tag;
  719.         });
  720.         return $this;
  721.     }
  722.     /**
  723.      * @internal
  724.      *
  725.      * @param array $tags
  726.      *
  727.      * @return $this
  728.      */
  729.     public function removeClearedTags(array $tags)
  730.     {
  731.         foreach ($tags as $tag) {
  732.             unset($this->clearedTags[$tag]);
  733.         }
  734.         return $this;
  735.     }
  736.     /**
  737.      * Writes save queue to the cache
  738.      *
  739.      * @internal
  740.      *
  741.      * @return bool
  742.      */
  743.     public function writeSaveQueue()
  744.     {
  745.         $totalResult true;
  746.         if ($this->writeLock->hasLock()) {
  747.             if (count($this->saveQueue) > 0) {
  748.                 $this->logger->debug(
  749.                     'Not writing save queue as there\'s an active write lock. Save queue contains {saveQueueCount} items.',
  750.                     ['saveQueueCount' => count($this->saveQueue)]
  751.                 );
  752.             }
  753.             return false;
  754.         }
  755.         $this->cleanupQueue();
  756.         $processedKeys = [];
  757.         foreach ($this->saveQueue as $queueItem) {
  758.             $key $queueItem->getKey();
  759.             // check if key was already processed and don't save it again
  760.             if (in_array($key$processedKeys)) {
  761.                 $this->logger->warning('Not writing item as key {key} was already processed', ['key' => $key]);
  762.                 continue;
  763.             }
  764.             $tags $this->prepareCacheTags($queueItem->getKey(), $queueItem->getData(), $queueItem->getTags());
  765.             if (null === $tags) {
  766.                 $result false;
  767.             // item shouldn't go to the cache (either because it's tags are ignored or were cleared within this process) -> see $this->prepareCacheTags();
  768.             } else {
  769.                 $result $this->storeCacheData($queueItem->getKey(), $queueItem->getData(), $tags$queueItem->getLifetime(), $queueItem->isForce());
  770.             }
  771.             $processedKeys[] = $key;
  772.             $totalResult $totalResult && $result;
  773.         }
  774.         // reset
  775.         $this->saveQueue = [];
  776.         return $totalResult;
  777.     }
  778.     /**
  779.      * Shut down pimcore - write cache entries and clean up
  780.      *
  781.      * @internal
  782.      *
  783.      * @param bool $forceWrite
  784.      *
  785.      * @return $this
  786.      */
  787.     public function shutdown($forceWrite false)
  788.     {
  789.         // clear tags scheduled for the shutdown
  790.         $this->clearTagsOnShutdown();
  791.         $doWrite true;
  792.         // writes make only sense for HTTP(S)
  793.         // CLI are normally longer running scripts that tend to produce race conditions
  794.         // so CLI scripts are not writing to the cache at all
  795.         if ($this->isCli()) {
  796.             if (!($this->handleCli || $forceWrite)) {
  797.                 $doWrite false;
  798.                 $queueCount count($this->saveQueue);
  799.                 if ($queueCount 0) {
  800.                     $this->logger->debug(
  801.                         'Not writing save queue to cache as process is running in CLI mode. Save queue contains {saveQueueCount} items.',
  802.                         ['saveQueueCount' => count($this->saveQueue)]
  803.                     );
  804.                 }
  805.             }
  806.         }
  807.         // write collected items to cache backend
  808.         if ($doWrite) {
  809.             $this->writeSaveQueue();
  810.         }
  811.         // remove the write lock
  812.         $this->writeLock->removeLock();
  813.         return $this;
  814.     }
  815.     /**
  816.      * @codeCoverageIgnore
  817.      *
  818.      * @return bool
  819.      */
  820.     protected function isCli()
  821.     {
  822.         return php_sapi_name() === 'cli';
  823.     }
  824. }