vendor/shopware/core/Content/Category/SalesChannel/CachedNavigationRoute.php line 191

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\CategoryCollection;
  6. use Shopware\Core\Content\Category\Event\NavigationRouteCacheKeyEvent;
  7. use Shopware\Core\Content\Category\Event\NavigationRouteCacheTagsEvent;
  8. use Shopware\Core\Framework\Adapter\Cache\AbstractCacheTracer;
  9. use Shopware\Core\Framework\Adapter\Cache\CacheCompressor;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Cache\EntityCacheKeyGenerator;
  11. use Shopware\Core\Framework\DataAbstractionLayer\FieldSerializer\JsonFieldSerializer;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  13. use Shopware\Core\Framework\Routing\Annotation\Entity;
  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 Shopware\Core\System\SalesChannel\StoreApiResponse;
  18. use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
  19. use Symfony\Component\HttpFoundation\Request;
  20. use Symfony\Component\Routing\Annotation\Route;
  21. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  22. /**
  23.  * @RouteScope(scopes={"store-api"})
  24.  */
  25. class CachedNavigationRoute extends AbstractNavigationRoute
  26. {
  27.     public const ALL_TAG 'navigation';
  28.     public const BASE_NAVIGATION_TAG 'base-navigation';
  29.     private AbstractNavigationRoute $decorated;
  30.     private TagAwareAdapterInterface $cache;
  31.     private EntityCacheKeyGenerator $generator;
  32.     /**
  33.      * @var AbstractCacheTracer<NavigationRouteResponse>
  34.      */
  35.     private AbstractCacheTracer $tracer;
  36.     private array $states;
  37.     private EventDispatcherInterface $dispatcher;
  38.     private LoggerInterface $logger;
  39.     /**
  40.      * @param AbstractCacheTracer<NavigationRouteResponse> $tracer
  41.      */
  42.     public function __construct(
  43.         AbstractNavigationRoute $decorated,
  44.         TagAwareAdapterInterface $cache,
  45.         EntityCacheKeyGenerator $generator,
  46.         AbstractCacheTracer $tracer,
  47.         EventDispatcherInterface $dispatcher,
  48.         array $states,
  49.         LoggerInterface $logger
  50.     ) {
  51.         $this->decorated $decorated;
  52.         $this->cache $cache;
  53.         $this->generator $generator;
  54.         $this->tracer $tracer;
  55.         $this->states $states;
  56.         $this->dispatcher $dispatcher;
  57.         $this->logger $logger;
  58.     }
  59.     public function getDecorated(): AbstractNavigationRoute
  60.     {
  61.         return $this->decorated;
  62.     }
  63.     /**
  64.      * @Since("6.2.0.0")
  65.      * @Entity("category")
  66.      * @OA\Post(
  67.      *      path="/navigation/{requestActiveId}/{requestRootId}",
  68.      *      summary="Fetch a navigation menu",
  69.      *      description="This endpoint returns categories that can be used as a page navigation. You can either return them as a tree or as a flat list. You can also control the depth of the tree.
  70. Instead of passing uuids, you can also use one of the following aliases for the activeId and rootId parameters to get the respective navigations of your sales channel.
  71. * main-navigation
  72. * service-navigation
  73. * footer-navigation",
  74.      *      operationId="readNavigation",
  75.      *      tags={"Store API", "Category"},
  76.      *      @OA\Parameter(name="Api-Basic-Parameters"),
  77.      *      @OA\Parameter(
  78.      *          name="sw-include-seo-urls",
  79.      *          description="Instructs Shopware to try and resolve SEO URLs for the given navigation item",
  80.      *          @OA\Schema(type="boolean"),
  81.      *          in="header",
  82.      *          required=false
  83.      *      ),
  84.      *      @OA\Parameter(
  85.      *          name="requestActiveId",
  86.      *          description="Identifier of the active category in the navigation tree (if not used, just set to the same as rootId).",
  87.      *          @OA\Schema(type="string", pattern="^[0-9a-f]{32}$"),
  88.      *          in="path",
  89.      *          required=true
  90.      *      ),
  91.      *      @OA\Parameter(
  92.      *          name="requestRootId",
  93.      *          description="Identifier of the root category for your desired navigation tree. You can use it to fetch sub-trees of your navigation tree.",
  94.      *          @OA\Schema(type="string", pattern="^[0-9a-f]{32}$"),
  95.      *          in="path",
  96.      *          required=true
  97.      *      ),
  98.      *      @OA\RequestBody(
  99.      *          required=true,
  100.      *          @OA\JsonContent(
  101.      *              @OA\Property(
  102.      *                  property="depth",
  103.      *                  description="Determines the depth of fetched navigation levels.",
  104.      *                  @OA\Schema(type="integer", default="2")
  105.      *              ),
  106.      *              @OA\Property(
  107.      *                  property="buildTree",
  108.      *                  description="Return the categories as a tree or as a flat list.",
  109.      *                  @OA\Schema(type="boolean", default="true")
  110.      *              )
  111.      *          )
  112.      *      ),
  113.      *      @OA\Response(
  114.      *          response="200",
  115.      *          description="All available navigations",
  116.      *          @OA\JsonContent(ref="#/components/schemas/NavigationRouteResponse")
  117.      *     )
  118.      * )
  119.      * @Route("/store-api/navigation/{activeId}/{rootId}", name="store-api.navigation", methods={"GET", "POST"})
  120.      */
  121.     public function load(string $activeIdstring $rootIdRequest $requestSalesChannelContext $contextCriteria $criteria): NavigationRouteResponse
  122.     {
  123.         if ($context->hasState(...$this->states)) {
  124.             $this->logger->info('cache-miss: ' self::buildName($activeId));
  125.             return $this->getDecorated()->load($activeId$rootId$request$context$criteria);
  126.         }
  127.         $depth $request->query->getInt('depth'$request->request->getInt('depth'2));
  128.         // first we load the base navigation, the base navigation is shared for all storefront listings
  129.         $response $this->loadNavigation($request$rootId$rootId$depth$context$criteria, [self::ALL_TAGself::BASE_NAVIGATION_TAG]);
  130.         // no we have to check if the active category is loaded and the children of the active category are loaded
  131.         if ($this->isActiveLoaded($rootId$response->getCategories(), $activeId)) {
  132.             return $response;
  133.         }
  134.         // reload missing children of active category, depth 0 allows us the skip base navigation loading in the core route
  135.         $active $this->loadNavigation($request$activeId$rootId0$context$criteria, [self::ALL_TAG]);
  136.         $response->getCategories()->merge($active->getCategories());
  137.         return $response;
  138.     }
  139.     public static function buildName(string $id): string
  140.     {
  141.         return 'navigation-route-' $id;
  142.     }
  143.     private function loadNavigation(Request $requeststring $activestring $rootIdint $depthSalesChannelContext $contextCriteria $criteria, array $tags = []): NavigationRouteResponse
  144.     {
  145.         $item $this->cache->getItem(
  146.             $this->generateKey($active$rootId$depth$request$context$criteria)
  147.         );
  148.         try {
  149.             if ($item->isHit() && $item->get()) {
  150.                 $this->logger->info('cache-hit: ' self::buildName($active));
  151.                 return CacheCompressor::uncompress($item);
  152.             }
  153.         } catch (\Throwable $e) {
  154.             $this->logger->error($e->getMessage());
  155.         }
  156.         $this->logger->info('cache-miss: ' self::buildName($active));
  157.         $request->query->set('depth', (string) $depth);
  158.         $name self::buildName($active);
  159.         $response $this->tracer->trace($name, function () use ($active$rootId$request$context$criteria) {
  160.             return $this->getDecorated()->load($active$rootId$request$context$criteria);
  161.         });
  162.         $item CacheCompressor::compress($item$response);
  163.         $item->tag($this->generateTags($tags$active$rootId$depth$request$response$context$criteria));
  164.         $this->cache->save($item);
  165.         return $response;
  166.     }
  167.     private function isActiveLoaded(string $rootCategoryCollection $categoriesstring $activeId): bool
  168.     {
  169.         if ($root === $activeId) {
  170.             return true;
  171.         }
  172.         $active $categories->get($activeId);
  173.         if ($active === null) {
  174.             return false;
  175.         }
  176.         if ($active->getChildCount() === 0) {
  177.             return $categories->has($active->getParentId());
  178.         }
  179.         foreach ($categories as $category) {
  180.             if ($category->getParentId() === $activeId) {
  181.                 return true;
  182.             }
  183.         }
  184.         return false;
  185.     }
  186.     private function generateKey(string $activestring $rootIdint $depthRequest $requestSalesChannelContext $contextCriteria $criteria): string
  187.     {
  188.         $parts = [
  189.             self::buildName($active),
  190.             $rootId,
  191.             $depth,
  192.             $this->generator->getCriteriaHash($criteria),
  193.             $this->generator->getSalesChannelContextHash($context),
  194.         ];
  195.         $event = new NavigationRouteCacheKeyEvent($parts$active$rootId$depth$request$context$criteria);
  196.         $this->dispatcher->dispatch($event);
  197.         return md5(JsonFieldSerializer::encodeJson($event->getParts()));
  198.     }
  199.     private function generateTags(array $tagsstring $activestring $rootIdint $depthRequest $requestStoreApiResponse $responseSalesChannelContext $contextCriteria $criteria): array
  200.     {
  201.         $tags array_merge(
  202.             $tags,
  203.             $this->tracer->get(self::buildName($context->getSalesChannelId())),
  204.             [self::buildName($context->getSalesChannelId())]
  205.         );
  206.         $event = new NavigationRouteCacheTagsEvent($tags$active$rootId$depth$request$response$context$criteria);
  207.         $this->dispatcher->dispatch($event);
  208.         return array_unique(array_filter($event->getTags()));
  209.     }
  210. }