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

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 bool|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(), 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.             $this->logger->error(
  505.                 'Failed to add entry {key} to cache. Item size was {itemSize}',
  506.                 [
  507.                     'key' => $item->getKey(),
  508.                     'itemSize' => formatBytes(strlen($item->get())),
  509.                 ]
  510.             );
  511.         }
  512.         $this->writeInProgress false;
  513.         return $result;
  514.     }
  515.     /**
  516.      * Remove a cache item
  517.      *
  518.      * @param string $key
  519.      *
  520.      * @return bool
  521.      */
  522.     public function remove($key)
  523.     {
  524.         CacheItem::validateKey($key);
  525.         $this->writeLock->lock();
  526.         return $this->pool->deleteItem($key);
  527.     }
  528.     /**
  529.      * Empty the cache
  530.      *
  531.      * @return bool
  532.      */
  533.     public function clearAll()
  534.     {
  535.         $this->writeLock->lock();
  536.         $this->logger->info('Clearing the whole cache');
  537.         $result $this->pool->clear();
  538.         // immediately acquire the write lock again (force), because the lock is in the cache too
  539.         $this->writeLock->lock(true);
  540.         // set state to cache cleared - prevents new items being written to cache
  541.         $this->cacheCleared true;
  542.         return $result;
  543.     }
  544.     /**
  545.      * @param string $tag
  546.      *
  547.      * @return bool
  548.      */
  549.     public function clearTag($tag)
  550.     {
  551.         return $this->clearTags([$tag]);
  552.     }
  553.     /**
  554.      * @param string[] $tags
  555.      *
  556.      * @return bool
  557.      */
  558.     public function clearTags(array $tags): bool
  559.     {
  560.         $this->writeLock->lock();
  561.         $originalTags $tags;
  562.         $this->logger->debug(
  563.             'Clearing cache tags',
  564.             ['tags' => $tags]
  565.         );
  566.         $tags $this->normalizeClearTags($tags);
  567.         if (count($tags) > 0) {
  568.             $result $this->pool->invalidateTags($tags);
  569.             $this->addClearedTags($tags);
  570.             return $result;
  571.         }
  572.         $this->logger->warning(
  573.             'Could not clear tags as tag list is empty after normalization',
  574.             [
  575.                 'tags' => $tags,
  576.                 'originalTags' => $originalTags,
  577.             ]
  578.         );
  579.         return false;
  580.     }
  581.     /**
  582.      * Clears all tags stored in tagsClearedOnShutdown, this function is executed during Pimcore shutdown
  583.      *
  584.      * @return bool
  585.      */
  586.     public function clearTagsOnShutdown()
  587.     {
  588.         if (empty($this->tagsClearedOnShutdown)) {
  589.             return true;
  590.         }
  591.         $this->logger->debug('Clearing shutdown cache tags', ['tags' => $this->tagsClearedOnShutdown]);
  592.         $result $this->pool->invalidateTags($this->tagsClearedOnShutdown);
  593.         $this->addClearedTags($this->tagsClearedOnShutdown);
  594.         $this->tagsClearedOnShutdown = [];
  595.         return $result;
  596.     }
  597.     /**
  598.      * Normalize (unique) clear tags and shift special tags to shutdown (e.g. output)
  599.      *
  600.      * @param array $tags
  601.      *
  602.      * @return array
  603.      */
  604.     protected function normalizeClearTags(array $tags)
  605.     {
  606.         $blacklist $this->tagsIgnoredOnClear;
  607.         // Shutdown tags are special tags being shifted to shutdown when scheduled to clear via clearTags. Explanation for
  608.         // the "output" tag:
  609.         // check for the tag output, because items with this tags are only cleared after the process is finished
  610.         // the reason is that eg. long running importers will clean the output-cache on every save/update, that's not necessary,
  611.         // only cleaning the output-cache on shutdown should be enough
  612.         foreach ($this->shutdownTags as $shutdownTag) {
  613.             if (in_array($shutdownTag$tags)) {
  614.                 $this->addTagClearedOnShutdown($shutdownTag);
  615.                 $blacklist[] = $shutdownTag;
  616.             }
  617.         }
  618.         // ensure that every tag is unique
  619.         $tags array_unique($tags);
  620.         // don't clear tags in ignore array
  621.         $tags array_filter($tags, function ($tag) use ($blacklist) {
  622.             return !in_array($tag$blacklist);
  623.         });
  624.         return $tags;
  625.     }
  626.     /**
  627.      * Add tag to list of cleared tags (internal use only)
  628.      *
  629.      * @param string|array $tags
  630.      *
  631.      * @return $this
  632.      */
  633.     protected function addClearedTags($tags)
  634.     {
  635.         if (!is_array($tags)) {
  636.             $tags = [$tags];
  637.         }
  638.         foreach ($tags as $tag) {
  639.             $this->clearedTags[$tag] = true;
  640.         }
  641.         return $this;
  642.     }
  643.     /**
  644.      * Adds a tag to the shutdown queue, see clearTagsOnShutdown
  645.      *
  646.      * @internal
  647.      *
  648.      * @param string $tag
  649.      *
  650.      * @return $this
  651.      */
  652.     public function addTagClearedOnShutdown($tag)
  653.     {
  654.         $this->writeLock->lock();
  655.         $this->tagsClearedOnShutdown[] = $tag;
  656.         $this->tagsClearedOnShutdown array_unique($this->tagsClearedOnShutdown);
  657.         return $this;
  658.     }
  659.     /**
  660.      * @internal
  661.      *
  662.      * @param string $tag
  663.      *
  664.      * @return $this
  665.      */
  666.     public function addTagIgnoredOnSave($tag)
  667.     {
  668.         $this->tagsIgnoredOnSave[] = $tag;
  669.         $this->tagsIgnoredOnSave array_unique($this->tagsIgnoredOnSave);
  670.         return $this;
  671.     }
  672.     /**
  673.      * @internal
  674.      *
  675.      * @param string $tag
  676.      *
  677.      * @return $this
  678.      */
  679.     public function removeTagIgnoredOnSave($tag)
  680.     {
  681.         $this->tagsIgnoredOnSave array_filter($this->tagsIgnoredOnSave, function ($t) use ($tag) {
  682.             return $t !== $tag;
  683.         });
  684.         return $this;
  685.     }
  686.     /**
  687.      * @internal
  688.      *
  689.      * @param string $tag
  690.      *
  691.      * @return $this
  692.      */
  693.     public function addTagIgnoredOnClear($tag)
  694.     {
  695.         $this->tagsIgnoredOnClear[] = $tag;
  696.         $this->tagsIgnoredOnClear array_unique($this->tagsIgnoredOnClear);
  697.         return $this;
  698.     }
  699.     /**
  700.      * @internal
  701.      *
  702.      * @param string $tag
  703.      *
  704.      * @return $this
  705.      */
  706.     public function removeTagIgnoredOnClear($tag)
  707.     {
  708.         $this->tagsIgnoredOnClear array_filter($this->tagsIgnoredOnClear, function ($t) use ($tag) {
  709.             return $t !== $tag;
  710.         });
  711.         return $this;
  712.     }
  713.     /**
  714.      * Writes save queue to the cache
  715.      *
  716.      * @internal
  717.      *
  718.      * @return bool
  719.      */
  720.     public function writeSaveQueue()
  721.     {
  722.         $totalResult true;
  723.         if ($this->writeLock->hasLock()) {
  724.             if (count($this->saveQueue) > 0) {
  725.                 $this->logger->debug(
  726.                     'Not writing save queue as there\'s an active write lock. Save queue contains {saveQueueCount} items.',
  727.                     ['saveQueueCount' => count($this->saveQueue)]
  728.                 );
  729.             }
  730.             return false;
  731.         }
  732.         $this->cleanupQueue();
  733.         $processedKeys = [];
  734.         foreach ($this->saveQueue as $queueItem) {
  735.             $key $queueItem->getKey();
  736.             // check if key was already processed and don't save it again
  737.             if (in_array($key$processedKeys)) {
  738.                 $this->logger->warning('Not writing item as key {key} was already processed', ['key' => $key]);
  739.                 continue;
  740.             }
  741.             $tags $this->prepareCacheTags($queueItem->getKey(), $queueItem->getData(), $queueItem->getTags());
  742.             if (null === $tags) {
  743.                 $result false;
  744.             // item shouldn't go to the cache (either because it's tags are ignored or were cleared within this process) -> see $this->prepareCacheTags();
  745.             } else {
  746.                 $result $this->storeCacheData($queueItem->getKey(), $queueItem->getData(), $tags$queueItem->getLifetime(), $queueItem->isForce());
  747.             }
  748.             $processedKeys[] = $key;
  749.             $totalResult $totalResult && $result;
  750.         }
  751.         // reset
  752.         $this->saveQueue = [];
  753.         return $totalResult;
  754.     }
  755.     /**
  756.      * Shut down pimcore - write cache entries and clean up
  757.      *
  758.      * @internal
  759.      *
  760.      * @param bool $forceWrite
  761.      *
  762.      * @return $this
  763.      */
  764.     public function shutdown($forceWrite false)
  765.     {
  766.         // clear tags scheduled for the shutdown
  767.         $this->clearTagsOnShutdown();
  768.         $doWrite true;
  769.         // writes make only sense for HTTP(S)
  770.         // CLI are normally longer running scripts that tend to produce race conditions
  771.         // so CLI scripts are not writing to the cache at all
  772.         if ($this->isCli()) {
  773.             if (!($this->handleCli || $forceWrite)) {
  774.                 $doWrite false;
  775.                 $queueCount count($this->saveQueue);
  776.                 if ($queueCount 0) {
  777.                     $this->logger->debug(
  778.                         'Not writing save queue to cache as process is running in CLI mode. Save queue contains {saveQueueCount} items.',
  779.                         ['saveQueueCount' => count($this->saveQueue)]
  780.                     );
  781.                 }
  782.             }
  783.         }
  784.         // write collected items to cache backend
  785.         if ($doWrite) {
  786.             $this->writeSaveQueue();
  787.         }
  788.         // remove the write lock
  789.         $this->writeLock->removeLock();
  790.         return $this;
  791.     }
  792.     /**
  793.      * @codeCoverageIgnore
  794.      *
  795.      * @return bool
  796.      */
  797.     protected function isCli()
  798.     {
  799.         return php_sapi_name() === 'cli';
  800.     }
  801. }