vendor/shopware/core/Content/Category/SalesChannel/CachedCategoryRoute.php line 103

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Category\SalesChannel;
  3. use Shopware\Core\Content\Category\Event\CategoryRouteCacheKeyEvent;
  4. use Shopware\Core\Content\Category\Event\CategoryRouteCacheTagsEvent;
  5. use Shopware\Core\Content\Cms\Aggregate\CmsSlot\CmsSlotEntity;
  6. use Shopware\Core\Content\Cms\SalesChannel\Struct\ProductBoxStruct;
  7. use Shopware\Core\Content\Cms\SalesChannel\Struct\ProductSliderStruct;
  8. use Shopware\Core\Framework\Adapter\Cache\AbstractCacheTracer;
  9. use Shopware\Core\Framework\Adapter\Cache\CacheValueCompressor;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Cache\EntityCacheKeyGenerator;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\RuleAreas;
  12. use Shopware\Core\Framework\DataAbstractionLayer\FieldSerializer\JsonFieldSerializer;
  13. use Shopware\Core\Framework\Log\Package;
  14. use Shopware\Core\Framework\Routing\Annotation\RouteScope;
  15. use Shopware\Core\Framework\Routing\Annotation\Since;
  16. use Shopware\Core\Profiling\Profiler;
  17. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  18. use Symfony\Component\HttpFoundation\Request;
  19. use Symfony\Component\Routing\Annotation\Route;
  20. use Symfony\Contracts\Cache\CacheInterface;
  21. use Symfony\Contracts\Cache\ItemInterface;
  22. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  23. /**
  24. * @Route(defaults={"_routeScope"={"store-api"}})
  25. */
  26. #[Package('content')]
  27. class CachedCategoryRoute extends AbstractCategoryRoute
  28. {
  29. private AbstractCategoryRoute $decorated;
  30. private CacheInterface $cache;
  31. private EntityCacheKeyGenerator $generator;
  32. /**
  33. * @var AbstractCacheTracer<CategoryRouteResponse>
  34. */
  35. private AbstractCacheTracer $tracer;
  36. /**
  37. * @var array<string>
  38. */
  39. private array $states;
  40. private EventDispatcherInterface $dispatcher;
  41. /**
  42. * @internal
  43. *
  44. * @param AbstractCacheTracer<CategoryRouteResponse> $tracer
  45. * @param array<string> $states
  46. */
  47. public function __construct(
  48. AbstractCategoryRoute $decorated,
  49. CacheInterface $cache,
  50. EntityCacheKeyGenerator $generator,
  51. AbstractCacheTracer $tracer,
  52. EventDispatcherInterface $dispatcher,
  53. array $states
  54. ) {
  55. $this->decorated = $decorated;
  56. $this->cache = $cache;
  57. $this->generator = $generator;
  58. $this->tracer = $tracer;
  59. $this->states = $states;
  60. $this->dispatcher = $dispatcher;
  61. }
  62. public static function buildName(string $id): string
  63. {
  64. return 'category-route-' . $id;
  65. }
  66. public function getDecorated(): AbstractCategoryRoute
  67. {
  68. return $this->decorated;
  69. }
  70. /**
  71. * @Since("6.2.0.0")
  72. * @Route("/store-api/category/{navigationId}", name="store-api.category.detail", methods={"GET","POST"})
  73. */
  74. public function load(string $navigationId, Request $request, SalesChannelContext $context): CategoryRouteResponse
  75. {
  76. return Profiler::trace('category-route', function () use ($navigationId, $request, $context) {
  77. if ($context->hasState(...$this->states)) {
  78. return $this->getDecorated()->load($navigationId, $request, $context);
  79. }
  80. $key = $this->generateKey($navigationId, $request, $context);
  81. if ($key === null) {
  82. return $this->getDecorated()->load($navigationId, $request, $context);
  83. }
  84. $value = $this->cache->get($key, function (ItemInterface $item) use ($navigationId, $request, $context) {
  85. $name = self::buildName($navigationId);
  86. $response = $this->tracer->trace($name, function () use ($navigationId, $request, $context) {
  87. return $this->getDecorated()->load($navigationId, $request, $context);
  88. });
  89. $item->tag($this->generateTags($navigationId, $response, $request, $context));
  90. return CacheValueCompressor::compress($response);
  91. });
  92. return CacheValueCompressor::uncompress($value);
  93. });
  94. }
  95. private function generateKey(string $navigationId, Request $request, SalesChannelContext $context): ?string
  96. {
  97. $parts = array_merge(
  98. $request->query->all(),
  99. $request->request->all(),
  100. [$this->generator->getSalesChannelContextHash($context, [RuleAreas::CATEGORY_AREA, RuleAreas::PRODUCT_AREA])]
  101. );
  102. $event = new CategoryRouteCacheKeyEvent($navigationId, $parts, $request, $context, null);
  103. $this->dispatcher->dispatch($event);
  104. if (!$event->shouldCache()) {
  105. return null;
  106. }
  107. return self::buildName($navigationId) . '-' . md5(JsonFieldSerializer::encodeJson($event->getParts()));
  108. }
  109. /**
  110. * @return array<string>
  111. */
  112. private function generateTags(string $navigationId, CategoryRouteResponse $response, Request $request, SalesChannelContext $context): array
  113. {
  114. $tags = array_merge(
  115. $this->tracer->get(self::buildName($navigationId)),
  116. $this->extractProductIds($response),
  117. [self::buildName($navigationId)]
  118. );
  119. $event = new CategoryRouteCacheTagsEvent($navigationId, $tags, $request, $response, $context, null);
  120. $this->dispatcher->dispatch($event);
  121. return array_unique(array_filter($event->getTags()));
  122. }
  123. /**
  124. * @return array<string>
  125. */
  126. private function extractProductIds(CategoryRouteResponse $response): array
  127. {
  128. $page = $response->getCategory()->getCmsPage();
  129. if ($page === null) {
  130. return [];
  131. }
  132. $ids = [];
  133. $streamIds = [];
  134. $slots = $page->getElementsOfType('product-slider');
  135. /** @var CmsSlotEntity $slot */
  136. foreach ($slots as $slot) {
  137. $slider = $slot->getData();
  138. if (!$slider instanceof ProductSliderStruct) {
  139. continue;
  140. }
  141. if ($slider->getStreamId() !== null) {
  142. $streamIds[] = $slider->getStreamId();
  143. }
  144. if ($slider->getProducts() === null) {
  145. continue;
  146. }
  147. foreach ($slider->getProducts() as $product) {
  148. $ids[] = $product->getId();
  149. $ids[] = $product->getParentId();
  150. }
  151. }
  152. $slots = $page->getElementsOfType('product-box');
  153. /** @var CmsSlotEntity $slot */
  154. foreach ($slots as $slot) {
  155. $box = $slot->getData();
  156. if (!$box instanceof ProductBoxStruct) {
  157. continue;
  158. }
  159. if ($box->getProduct() === null) {
  160. continue;
  161. }
  162. $ids[] = $box->getProduct()->getId();
  163. $ids[] = $box->getProduct()->getParentId();
  164. }
  165. $ids = array_values(array_unique(array_filter($ids)));
  166. return array_merge(
  167. array_map([EntityCacheKeyGenerator::class, 'buildProductTag'], $ids),
  168. array_map([EntityCacheKeyGenerator::class, 'buildStreamTag'], $streamIds),
  169. [EntityCacheKeyGenerator::buildCmsTag($page->getId())]
  170. );
  171. }
  172. }