vendor/shopware/core/Content/Category/SalesChannel/NavigationRoute.php line 156

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Category\SalesChannel;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Content\Category\CategoryCollection;
  5. use Shopware\Core\Content\Category\CategoryEntity;
  6. use Shopware\Core\Content\Category\Exception\CategoryNotFoundException;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\TermsAggregation;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\CountAggregation;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Bucket\TermsResult;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
  15. use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
  16. use Shopware\Core\Framework\Routing\Annotation\Entity;
  17. use Shopware\Core\Framework\Routing\Annotation\RouteScope;
  18. use Shopware\Core\Framework\Routing\Annotation\Since;
  19. use Shopware\Core\Framework\Uuid\Uuid;
  20. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
  21. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  22. use Symfony\Component\HttpFoundation\Request;
  23. use Symfony\Component\Routing\Annotation\Route;
  24. /**
  25.  * @Route(defaults={"_routeScope"={"store-api"}})
  26.  */
  27. class NavigationRoute extends AbstractNavigationRoute
  28. {
  29.     /**
  30.      * @var SalesChannelRepositoryInterface
  31.      */
  32.     private $categoryRepository;
  33.     /**
  34.      * @var Connection
  35.      */
  36.     private $connection;
  37.     /**
  38.      * @internal
  39.      */
  40.     public function __construct(
  41.         Connection $connection,
  42.         SalesChannelRepositoryInterface $repository
  43.     ) {
  44.         $this->categoryRepository $repository;
  45.         $this->connection $connection;
  46.     }
  47.     public function getDecorated(): AbstractNavigationRoute
  48.     {
  49.         throw new DecorationPatternException(self::class);
  50.     }
  51.     /**
  52.      * @Since("6.2.0.0")
  53.      * @Entity("category")
  54.      * @Route("/store-api/navigation/{activeId}/{rootId}", name="store-api.navigation", methods={"GET", "POST"})
  55.      */
  56.     public function load(
  57.         string $activeId,
  58.         string $rootId,
  59.         Request $request,
  60.         SalesChannelContext $context,
  61.         Criteria $criteria
  62.     ): NavigationRouteResponse {
  63.         $depth $request->query->getInt('depth'$request->request->getInt('depth'2));
  64.         $metaInfo $this->getCategoryMetaInfo($activeId$rootId);
  65.         $active $this->getMetaInfoById($activeId$metaInfo);
  66.         $root $this->getMetaInfoById($rootId$metaInfo);
  67.         // Validate the provided category is part of the sales channel
  68.         $this->validate($activeId$active['path'], $context);
  69.         $isChild $this->isChildCategory($activeId$active['path'], $rootId);
  70.         // If the provided activeId is not part of the rootId, a fallback to the rootId must be made here.
  71.         // The passed activeId is therefore part of another navigation and must therefore not be loaded.
  72.         // The availability validation has already been done in the `validate` function.
  73.         if (!$isChild) {
  74.             $activeId $rootId;
  75.         }
  76.         $categories = new CategoryCollection();
  77.         if ($depth 0) {
  78.             // Load the first two levels without using the activeId in the query
  79.             $categories $this->loadLevels($rootId, (int) $root['level'], $context, clone $criteria$depth);
  80.         }
  81.         // If the active category is part of the provided root id, we have to load the children and the parents of the active id
  82.         $categories $this->loadChildren($activeId$context$rootId$metaInfo$categories, clone $criteria);
  83.         return new NavigationRouteResponse($categories);
  84.     }
  85.     private function loadCategories(array $idsSalesChannelContext $contextCriteria $criteria): CategoryCollection
  86.     {
  87.         $criteria->setIds($ids);
  88.         $criteria->addAssociation('media');
  89.         $criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_NONE);
  90.         /** @var CategoryCollection $missing */
  91.         $missing $this->categoryRepository->search($criteria$context)->getEntities();
  92.         return $missing;
  93.     }
  94.     private function loadLevels(string $rootIdint $rootLevelSalesChannelContext $contextCriteria $criteriaint $depth 2): CategoryCollection
  95.     {
  96.         $criteria->addFilter(
  97.             new ContainsFilter('path''|' $rootId '|'),
  98.             new RangeFilter('level', [
  99.                 RangeFilter::GT => $rootLevel,
  100.                 RangeFilter::LTE => $rootLevel $depth 1,
  101.             ])
  102.         );
  103.         $criteria->addAssociation('media');
  104.         $criteria->setLimit(null);
  105.         $criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_NONE);
  106.         /** @var CategoryCollection $levels */
  107.         $levels $this->categoryRepository->search($criteria$context)->getEntities();
  108.         $this->addVisibilityCounts($rootId$rootLevel$depth$levels$context);
  109.         return $levels;
  110.     }
  111.     private function getCategoryMetaInfo(string $activeIdstring $rootId): array
  112.     {
  113.         $result $this->connection->fetchAllAssociative('
  114.             # navigation-route::meta-information
  115.             SELECT LOWER(HEX(`id`)), `path`, `level`
  116.             FROM `category`
  117.             WHERE `id` = :activeId OR `parent_id` = :activeId OR `id` = :rootId
  118.         ', ['activeId' => Uuid::fromHexToBytes($activeId), 'rootId' => Uuid::fromHexToBytes($rootId)]);
  119.         if (!$result) {
  120.             throw new CategoryNotFoundException($activeId);
  121.         }
  122.         return FetchModeHelper::groupUnique($result);
  123.     }
  124.     private function getMetaInfoById(string $id, array $metaInfo): array
  125.     {
  126.         if (!\array_key_exists($id$metaInfo)) {
  127.             throw new CategoryNotFoundException($id);
  128.         }
  129.         return $metaInfo[$id];
  130.     }
  131.     private function loadChildren(string $activeIdSalesChannelContext $contextstring $rootId, array $metaInfoCategoryCollection $categoriesCriteria $criteria): CategoryCollection
  132.     {
  133.         $active $this->getMetaInfoById($activeId$metaInfo);
  134.         unset($metaInfo[$rootId], $metaInfo[$activeId]);
  135.         $childIds array_keys($metaInfo);
  136.         // Fetch all parents and first-level children of the active category, if they're not already fetched
  137.         $missing $this->getMissingIds($activeId$active['path'], $childIds$categories);
  138.         if (empty($missing)) {
  139.             return $categories;
  140.         }
  141.         $categories->merge(
  142.             $this->loadCategories($missing$context$criteria)
  143.         );
  144.         return $categories;
  145.     }
  146.     /**
  147.      * @param array<string> $childIds
  148.      */
  149.     private function getMissingIds(string $activeId, ?string $path, array $childIdsCategoryCollection $alreadyLoaded): array
  150.     {
  151.         $parentIds array_filter(explode('|'$path ?? ''));
  152.         $haveToBeIncluded array_merge($childIds$parentIds, [$activeId]);
  153.         $included $alreadyLoaded->getIds();
  154.         $included array_flip($included);
  155.         return array_diff($haveToBeIncluded$included);
  156.     }
  157.     private function validate(string $activeId, ?string $pathSalesChannelContext $context): void
  158.     {
  159.         $ids array_filter([
  160.             $context->getSalesChannel()->getFooterCategoryId(),
  161.             $context->getSalesChannel()->getServiceCategoryId(),
  162.             $context->getSalesChannel()->getNavigationCategoryId(),
  163.         ]);
  164.         foreach ($ids as $id) {
  165.             if ($this->isChildCategory($activeId$path$id)) {
  166.                 return;
  167.             }
  168.         }
  169.         throw new CategoryNotFoundException($activeId);
  170.     }
  171.     private function isChildCategory(string $activeId, ?string $pathstring $rootId): bool
  172.     {
  173.         if ($rootId === $activeId) {
  174.             return true;
  175.         }
  176.         if ($path === null) {
  177.             return false;
  178.         }
  179.         if (mb_strpos($path'|' $rootId '|') !== false) {
  180.             return true;
  181.         }
  182.         return false;
  183.     }
  184.     private function addVisibilityCounts(string $rootIdint $rootLevelint $depthCategoryCollection $levelsSalesChannelContext $context): void
  185.     {
  186.         $counts = [];
  187.         foreach ($levels as $category) {
  188.             if (!$category->getActive() || !$category->getVisible()) {
  189.                 continue;
  190.             }
  191.             $parentId $category->getParentId();
  192.             $counts[$parentId] = $counts[$parentId] ?? 0;
  193.             ++$counts[$parentId];
  194.         }
  195.         foreach ($levels as $category) {
  196.             $category->setVisibleChildCount($counts[$category->getId()] ?? 0);
  197.         }
  198.         // Fetch additional level of categories for counting visible children that are NOT included in the original query
  199.         $criteria = new Criteria();
  200.         $criteria->addFilter(
  201.             new ContainsFilter('path''|' $rootId '|'),
  202.             new EqualsFilter('level'$rootLevel $depth 1),
  203.             new EqualsFilter('active'true),
  204.             new EqualsFilter('visible'true)
  205.         );
  206.         $criteria->addAggregation(
  207.             new TermsAggregation('category-ids''parentId'nullnull, new CountAggregation('visible-children-count''id'))
  208.         );
  209.         $termsResult $this->categoryRepository
  210.             ->aggregate($criteria$context)
  211.             ->get('category-ids');
  212.         if (!($termsResult instanceof TermsResult)) {
  213.             return;
  214.         }
  215.         foreach ($termsResult->getBuckets() as $bucket) {
  216.             $key $bucket->getKey();
  217.             if ($key === null) {
  218.                 continue;
  219.             }
  220.             $parent $levels->get($key);
  221.             if ($parent instanceof CategoryEntity) {
  222.                 $parent->setVisibleChildCount($bucket->getCount());
  223.             }
  224.         }
  225.     }
  226. }