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

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 $adapter, WriteLock $writeLock, EventDispatcherInterface $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 $a, CacheQueueItem $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. $blocklist = $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. $blocklist[] = $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 ($blocklist) {
  631. return !in_array($tag, $blocklist);
  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. }