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

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Category\SalesChannel;
  3. use Shopware\Core\Content\Category\CategoryCollection;
  4. use Shopware\Core\Content\Category\CategoryEntity;
  5. use Shopware\Core\Content\Category\Event\NavigationRouteCacheKeyEvent;
  6. use Shopware\Core\Content\Category\Event\NavigationRouteCacheTagsEvent;
  7. use Shopware\Core\Framework\Adapter\Cache\AbstractCacheTracer;
  8. use Shopware\Core\Framework\Adapter\Cache\CacheValueCompressor;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Cache\EntityCacheKeyGenerator;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\RuleAreas;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  12. use Shopware\Core\Framework\Log\Package;
  13. use Shopware\Core\Framework\Util\Json;
  14. use Shopware\Core\Profiling\Profiler;
  15. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  16. use Shopware\Core\System\SalesChannel\StoreApiResponse;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use Symfony\Component\Routing\Annotation\Route;
  19. use Symfony\Contracts\Cache\CacheInterface;
  20. use Symfony\Contracts\Cache\ItemInterface;
  21. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  22. #[Route(defaults: ['_routeScope' => ['store-api']])]
  23. #[Package('content')]
  24. class CachedNavigationRoute extends AbstractNavigationRoute
  25. {
  26.     final public const ALL_TAG 'navigation';
  27.     final public const BASE_NAVIGATION_TAG 'base-navigation';
  28.     /**
  29.      * @internal
  30.      *
  31.      * @param AbstractCacheTracer<NavigationRouteResponse> $tracer
  32.      * @param array<string> $states
  33.      */
  34.     public function __construct(
  35.         private readonly AbstractNavigationRoute $decorated,
  36.         private readonly CacheInterface $cache,
  37.         private readonly EntityCacheKeyGenerator $generator,
  38.         private readonly AbstractCacheTracer $tracer,
  39.         private readonly EventDispatcherInterface $dispatcher,
  40.         private readonly array $states
  41.     ) {
  42.     }
  43.     public function getDecorated(): AbstractNavigationRoute
  44.     {
  45.         return $this->decorated;
  46.     }
  47.     #[Route(path'/store-api/navigation/{activeId}/{rootId}'name'store-api.navigation'methods: ['GET''POST'], defaults: ['_entity' => 'category'])]
  48.     public function load(string $activeIdstring $rootIdRequest $requestSalesChannelContext $contextCriteria $criteria): NavigationRouteResponse
  49.     {
  50.         return Profiler::trace('navigation-route', function () use ($activeId$rootId$request$context$criteria) {
  51.             if ($context->hasState(...$this->states)) {
  52.                 return $this->getDecorated()->load($activeId$rootId$request$context$criteria);
  53.             }
  54.             $depth $request->query->getInt('depth'$request->request->getInt('depth'2));
  55.             // first we load the base navigation, the base navigation is shared for all storefront listings
  56.             $response $this->loadNavigation($request$rootId$rootId$depth$context$criteria, [self::ALL_TAGself::BASE_NAVIGATION_TAG]);
  57.             // no we have to check if the active category is loaded and the children of the active category are loaded
  58.             if ($this->isActiveLoaded($rootId$response->getCategories(), $activeId)) {
  59.                 return $response;
  60.             }
  61.             // reload missing children of active category, depth 0 allows us the skip base navigation loading in the core route
  62.             $active $this->loadNavigation($request$activeId$rootId0$context$criteria, [self::ALL_TAG]);
  63.             $response->getCategories()->merge($active->getCategories());
  64.             return $response;
  65.         });
  66.     }
  67.     public static function buildName(string $id): string
  68.     {
  69.         return 'navigation-route-' $id;
  70.     }
  71.     /**
  72.      * @param array<string> $tags
  73.      */
  74.     private function loadNavigation(Request $requeststring $activestring $rootIdint $depthSalesChannelContext $contextCriteria $criteria, array $tags = []): NavigationRouteResponse
  75.     {
  76.         $key $this->generateKey($active$rootId$depth$request$context$criteria);
  77.         if ($key === null) {
  78.             return $this->getDecorated()->load($active$rootId$request$context$criteria);
  79.         }
  80.         $value $this->cache->get($key, function (ItemInterface $item) use ($active$depth$rootId$request$context$criteria$tags) {
  81.             $request->query->set('depth', (string) $depth);
  82.             $name self::buildName($active);
  83.             $response $this->tracer->trace($name, fn () => $this->getDecorated()->load($active$rootId$request$context$criteria));
  84.             $item->tag($this->generateTags($tags$active$rootId$depth$request$response$context$criteria));
  85.             return CacheValueCompressor::compress($response);
  86.         });
  87.         return CacheValueCompressor::uncompress($value);
  88.     }
  89.     private function isActiveLoaded(string $rootCategoryCollection $categoriesstring $activeId): bool
  90.     {
  91.         if ($root === $activeId) {
  92.             return true;
  93.         }
  94.         $active $categories->get($activeId);
  95.         if (!$active instanceof CategoryEntity) {
  96.             return false;
  97.         }
  98.         if ($active->getChildCount() === && \is_string($active->getParentId())) {
  99.             return $categories->has($active->getParentId());
  100.         }
  101.         foreach ($categories as $category) {
  102.             if ($category->getParentId() === $activeId) {
  103.                 return true;
  104.             }
  105.         }
  106.         return false;
  107.     }
  108.     private function generateKey(string $activestring $rootIdint $depthRequest $requestSalesChannelContext $contextCriteria $criteria): ?string
  109.     {
  110.         $parts = [
  111.             $rootId,
  112.             $depth,
  113.             $this->generator->getCriteriaHash($criteria),
  114.             $this->generator->getSalesChannelContextHash($context, [RuleAreas::CATEGORY_AREA]),
  115.         ];
  116.         $event = new NavigationRouteCacheKeyEvent($parts$active$rootId$depth$request$context$criteria);
  117.         $this->dispatcher->dispatch($event);
  118.         if (!$event->shouldCache()) {
  119.             return null;
  120.         }
  121.         return self::buildName($active) . '-' md5(Json::encode($event->getParts()));
  122.     }
  123.     /**
  124.      * @param array<string> $tags
  125.      *
  126.      * @return array<string>
  127.      */
  128.     private function generateTags(array $tagsstring $activestring $rootIdint $depthRequest $requestStoreApiResponse $responseSalesChannelContext $contextCriteria $criteria): array
  129.     {
  130.         $tags array_merge(
  131.             $tags,
  132.             $this->tracer->get(self::buildName($context->getSalesChannelId())),
  133.             [self::buildName($context->getSalesChannelId())]
  134.         );
  135.         $event = new NavigationRouteCacheTagsEvent($tags$active$rootId$depth$request$response$context$criteria);
  136.         $this->dispatcher->dispatch($event);
  137.         return array_unique(array_filter($event->getTags()));
  138.     }
  139. }