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

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Category\SalesChannel;
  3. use OpenApi\Annotations as OA;
  4. use Psr\Log\LoggerInterface;
  5. use Shopware\Core\Content\Category\Event\CategoryRouteCacheKeyEvent;
  6. use Shopware\Core\Content\Category\Event\CategoryRouteCacheTagsEvent;
  7. use Shopware\Core\Content\Cms\Aggregate\CmsSlot\CmsSlotEntity;
  8. use Shopware\Core\Content\Cms\SalesChannel\Struct\ProductBoxStruct;
  9. use Shopware\Core\Content\Cms\SalesChannel\Struct\ProductSliderStruct;
  10. use Shopware\Core\Framework\Adapter\Cache\AbstractCacheTracer;
  11. use Shopware\Core\Framework\Adapter\Cache\CacheCompressor;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Cache\EntityCacheKeyGenerator;
  13. use Shopware\Core\Framework\DataAbstractionLayer\FieldSerializer\JsonFieldSerializer;
  14. use Shopware\Core\Framework\Routing\Annotation\RouteScope;
  15. use Shopware\Core\Framework\Routing\Annotation\Since;
  16. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  17. use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
  18. use Symfony\Component\HttpFoundation\Request;
  19. use Symfony\Component\Routing\Annotation\Route;
  20. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  21. /**
  22.  * @RouteScope(scopes={"store-api"})
  23.  */
  24. class CachedCategoryRoute extends AbstractCategoryRoute
  25. {
  26.     private AbstractCategoryRoute $decorated;
  27.     private TagAwareAdapterInterface $cache;
  28.     private EntityCacheKeyGenerator $generator;
  29.     /**
  30.      * @var AbstractCacheTracer<CategoryRouteResponse>
  31.      */
  32.     private AbstractCacheTracer $tracer;
  33.     private array $states;
  34.     private EventDispatcherInterface $dispatcher;
  35.     private LoggerInterface $logger;
  36.     /**
  37.      * @param AbstractCacheTracer<CategoryRouteResponse> $tracer
  38.      */
  39.     public function __construct(
  40.         AbstractCategoryRoute $decorated,
  41.         TagAwareAdapterInterface $cache,
  42.         EntityCacheKeyGenerator $generator,
  43.         AbstractCacheTracer $tracer,
  44.         EventDispatcherInterface $dispatcher,
  45.         array $states,
  46.         LoggerInterface $logger
  47.     ) {
  48.         $this->decorated $decorated;
  49.         $this->cache $cache;
  50.         $this->generator $generator;
  51.         $this->tracer $tracer;
  52.         $this->states $states;
  53.         $this->dispatcher $dispatcher;
  54.         $this->logger $logger;
  55.     }
  56.     public static function buildName(string $id): string
  57.     {
  58.         return 'category-route-' $id;
  59.     }
  60.     public function getDecorated(): AbstractCategoryRoute
  61.     {
  62.         return $this->decorated;
  63.     }
  64.     /**
  65.      * @Since("6.2.0.0")
  66.      * @OA\Post(
  67.      *     path="/category/{categoryId}",
  68.      *     summary="Fetch a single category",
  69.      *     description="This endpoint returns information about the category, as well as a fully resolved (hydrated with mapping values) CMS page, if one is assigned to the category. You can pass slots which should be resolved exclusively.",
  70.      *     operationId="readCategory",
  71.      *     tags={"Store API", "Category"},
  72.      *     @OA\Parameter(
  73.      *         name="categoryId",
  74.      *         description="Identifier of the category to be fetched",
  75.      *         @OA\Schema(type="string", pattern="^[0-9a-f]{32}$"),
  76.      *         in="path",
  77.      *         required=true
  78.      *     ),
  79.      *     @OA\Parameter(
  80.      *         name="slots",
  81.      *         description="Resolves only the given slot identifiers. The identifiers have to be seperated by a '|' character",
  82.      *         @OA\Schema(type="string"),
  83.      *         in="query",
  84.      *     ),
  85.      *     @OA\Parameter(name="Api-Basic-Parameters"),
  86.      *     @OA\Response(
  87.      *          response="200",
  88.      *          description="The loaded category with cms page",
  89.      *          @OA\JsonContent(ref="#/components/schemas/Category")
  90.      *     )
  91.      * )
  92.      *
  93.      * @Route("/store-api/category/{navigationId}", name="store-api.category.detail", methods={"GET","POST"})
  94.      */
  95.     public function load(string $navigationIdRequest $requestSalesChannelContext $context): CategoryRouteResponse
  96.     {
  97.         if ($context->hasState(...$this->states)) {
  98.             $this->logger->info('cache-miss: ' self::buildName($navigationId));
  99.             return $this->getDecorated()->load($navigationId$request$context);
  100.         }
  101.         $item $this->cache->getItem(
  102.             $this->generateKey($navigationId$request$context)
  103.         );
  104.         try {
  105.             if ($item->isHit() && $item->get()) {
  106.                 $this->logger->info('cache-hit: ' self::buildName($navigationId));
  107.                 return CacheCompressor::uncompress($item);
  108.             }
  109.         } catch (\Throwable $e) {
  110.             $this->logger->error($e->getMessage());
  111.         }
  112.         $this->logger->info('cache-miss: ' self::buildName($navigationId));
  113.         $name self::buildName($navigationId);
  114.         $response $this->tracer->trace($name, function () use ($navigationId$request$context) {
  115.             return $this->getDecorated()->load($navigationId$request$context);
  116.         });
  117.         $item CacheCompressor::compress($item$response);
  118.         $item->tag($this->generateTags($navigationId$response$request$context));
  119.         $this->cache->save($item);
  120.         return $response;
  121.     }
  122.     private function generateKey(string $navigationIdRequest $requestSalesChannelContext $context): string
  123.     {
  124.         $parts array_merge(
  125.             $request->query->all(),
  126.             $request->request->all(),
  127.             [
  128.                 self::buildName($navigationId),
  129.                 $this->generator->getSalesChannelContextHash($context),
  130.             ]
  131.         );
  132.         $event = new CategoryRouteCacheKeyEvent($navigationId$parts$request$contextnull);
  133.         $this->dispatcher->dispatch($event);
  134.         return md5(JsonFieldSerializer::encodeJson($event->getParts()));
  135.     }
  136.     private function generateTags(string $navigationIdCategoryRouteResponse $responseRequest $requestSalesChannelContext $context): array
  137.     {
  138.         $tags array_merge(
  139.             $this->tracer->get(self::buildName($navigationId)),
  140.             $this->extractProductIds($response),
  141.             [self::buildName($navigationId)]
  142.         );
  143.         $event = new CategoryRouteCacheTagsEvent($navigationId$tags$request$response$contextnull);
  144.         $this->dispatcher->dispatch($event);
  145.         return array_unique(array_filter($event->getTags()));
  146.     }
  147.     private function extractProductIds(CategoryRouteResponse $response): array
  148.     {
  149.         $page $response->getCategory()->getCmsPage();
  150.         if ($page === null) {
  151.             return [];
  152.         }
  153.         $ids = [];
  154.         $slots $page->getElementsOfType('product-slider');
  155.         /** @var CmsSlotEntity $slot */
  156.         foreach ($slots as $slot) {
  157.             $slider $slot->getData();
  158.             if (!$slider instanceof ProductSliderStruct) {
  159.                 continue;
  160.             }
  161.             if ($slider->getProducts() === null) {
  162.                 continue;
  163.             }
  164.             foreach ($slider->getProducts() as $product) {
  165.                 $ids[] = $product->getId();
  166.                 $ids[] = $product->getParentId();
  167.             }
  168.         }
  169.         $slots $page->getElementsOfType('product-box');
  170.         /** @var CmsSlotEntity $slot */
  171.         foreach ($slots as $slot) {
  172.             $box $slot->getData();
  173.             if (!$box instanceof ProductBoxStruct) {
  174.                 continue;
  175.             }
  176.             if ($box->getProduct() === null) {
  177.                 continue;
  178.             }
  179.             $ids[] = $box->getProduct()->getId();
  180.             $ids[] = $box->getProduct()->getParentId();
  181.         }
  182.         $ids array_values(array_unique(array_filter($ids)));
  183.         return array_merge(
  184.             array_map([EntityCacheKeyGenerator::class, 'buildProductTag'], $ids),
  185.             [EntityCacheKeyGenerator::buildCmsTag($page->getId())]
  186.         );
  187.     }
  188. }