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

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