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

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Category\SalesChannel;
  3. use Doctrine\DBAL\Connection;
  4. use OpenApi\Annotations as OA;
  5. use Shopware\Core\Content\Category\CategoryCollection;
  6. use Shopware\Core\Content\Category\Exception\CategoryNotFoundException;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Search\RequestCriteriaBuilder;
  12. use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
  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\Framework\Uuid\Uuid;
  17. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
  18. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  19. use Shopware\Core\System\SalesChannel\SalesChannelEntity;
  20. use Symfony\Component\HttpFoundation\Request;
  21. use Symfony\Component\Routing\Annotation\Route;
  22. /**
  23.  * @RouteScope(scopes={"store-api"})
  24.  */
  25. class NavigationRoute extends AbstractNavigationRoute
  26. {
  27.     /**
  28.      * @var SalesChannelRepositoryInterface
  29.      */
  30.     private $categoryRepository;
  31.     /**
  32.      * @var Connection
  33.      */
  34.     private $connection;
  35.     /**
  36.      * @var SalesChannelCategoryDefinition
  37.      */
  38.     private $categoryDefinition;
  39.     /**
  40.      * @var RequestCriteriaBuilder
  41.      */
  42.     private $requestCriteriaBuilder;
  43.     public function __construct(
  44.         Connection $connection,
  45.         SalesChannelRepositoryInterface $repository,
  46.         SalesChannelCategoryDefinition $categoryDefinition,
  47.         RequestCriteriaBuilder $requestCriteriaBuilder
  48.     ) {
  49.         $this->categoryRepository $repository;
  50.         $this->connection $connection;
  51.         $this->categoryDefinition $categoryDefinition;
  52.         $this->requestCriteriaBuilder $requestCriteriaBuilder;
  53.     }
  54.     public function getDecorated(): AbstractNavigationRoute
  55.     {
  56.         throw new DecorationPatternException(self::class);
  57.     }
  58.     /**
  59.      * @Since("6.2.0.0")
  60.      * @Entity("category")
  61.      * @OA\Post(
  62.      *      path="/navigation/{requestActiveId}/{requestRootId}",
  63.      *      summary="Loads all available navigations",
  64.      *      operationId="readNavigation",
  65.      *      tags={"Store API", "Navigation"},
  66.      *      @OA\Parameter(name="Api-Basic-Parameters"),
  67.      *      @OA\Parameter(name="requestActiveId", description="Active Category ID", @OA\Schema(type="string"), in="path", required=true),
  68.      *      @OA\Parameter(name="requestRootId", description="Root Category ID", @OA\Schema(type="string"), in="path", required=true),
  69.      *      @OA\RequestBody(
  70.      *          required=true,
  71.      *          @OA\JsonContent(
  72.      *              @OA\Property(property="buildTree", description="Build category tree", type="boolean")
  73.      *          )
  74.      *      ),
  75.      *      @OA\Response(
  76.      *          response="200",
  77.      *          description="All available navigations",
  78.      *          @OA\JsonContent(ref="#/components/schemas/NavigationRouteResponse")
  79.      *     )
  80.      * )
  81.      * @Route("/store-api/v{version}/navigation/{requestActiveId}/{requestRootId}", name="store-api.navigation", methods={"GET", "POST"})
  82.      */
  83.     public function load(
  84.         string $requestActiveId,
  85.         string $requestRootId,
  86.         Request $request,
  87.         SalesChannelContext $context,
  88.         ?Criteria $criteria null
  89.     ): NavigationRouteResponse {
  90.         $buildTree $request->query->getBoolean('buildTree'$request->request->getBoolean('buildTree'true));
  91.         $depth $request->query->getInt('depth'$request->request->getInt('depth'2));
  92.         $activeId $this->resolveAliasId($requestActiveId$context->getSalesChannel());
  93.         $rootId $this->resolveAliasId($requestRootId$context->getSalesChannel());
  94.         if ($activeId === null) {
  95.             throw new CategoryNotFoundException($requestActiveId);
  96.         }
  97.         if ($rootId === null) {
  98.             throw new CategoryNotFoundException($requestRootId);
  99.         }
  100.         $metaInfo $this->getCategoryMetaInfo($activeId$rootId);
  101.         $active $this->getMetaInfoById($activeId$metaInfo);
  102.         $root $this->getMetaInfoById($rootId$metaInfo);
  103.         // Validate the provided category is part of the sales channel
  104.         $this->validate($activeId$active['path'], $context);
  105.         $isChild $this->isChildCategory($activeId$active['path'], $rootId);
  106.         // If the provided activeId is not part of the rootId, a fallback to the rootId must be made here.
  107.         // The passed activeId is therefore part of another navigation and must therefore not be loaded.
  108.         // The availability validation has already been done in the `validate` function.
  109.         if (!$isChild) {
  110.             $activeId $rootId;
  111.         }
  112.         // @deprecated tag:v6.4.0 - Criteria will be required
  113.         if (!$criteria) {
  114.             $criteria $this->requestCriteriaBuilder->handleRequest($request, new Criteria(), $this->categoryDefinition$context->getContext());
  115.         }
  116.         // Load the first two levels without using the activeId in the query, so this can be cached
  117.         $categories $this->loadLevels($rootId, (int) $root['level'], $context, clone $criteria$depth);
  118.         // If the active category is part of the provided root id, we have to load the children and the parents of the active id
  119.         $categories $this->loadChildren($activeId$context$rootId$metaInfo$categories, clone $criteria);
  120.         if ($buildTree) {
  121.             $categories $this->buildTree($rootId$categories->getElements());
  122.         }
  123.         return new NavigationRouteResponse($categories);
  124.     }
  125.     private function buildTree(?string $parentId, array $categories): CategoryCollection
  126.     {
  127.         $children = new CategoryCollection();
  128.         foreach ($categories as $key => $category) {
  129.             if ($category->getParentId() !== $parentId) {
  130.                 continue;
  131.             }
  132.             unset($categories[$key]);
  133.             $children->add($category);
  134.         }
  135.         $children->sortByPosition();
  136.         $items = new CategoryCollection();
  137.         foreach ($children as $child) {
  138.             if (!$child->getActive() || !$child->getVisible()) {
  139.                 continue;
  140.             }
  141.             $child->setChildren($this->buildTree($child->getId(), $categories));
  142.             $items->add($child);
  143.         }
  144.         return $items;
  145.     }
  146.     private function loadCategories(array $idsSalesChannelContext $contextCriteria $criteria): CategoryCollection
  147.     {
  148.         $criteria->setIds($ids);
  149.         $criteria->addAssociation('media');
  150.         $criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_NONE);
  151.         /** @var CategoryCollection $missing */
  152.         $missing $this->categoryRepository->search($criteria$context)->getEntities();
  153.         return $missing;
  154.     }
  155.     private function loadLevels(string $rootIdint $rootLevelSalesChannelContext $contextCriteria $criteriaint $depth 2): CategoryCollection
  156.     {
  157.         $criteria->addFilter(
  158.             new ContainsFilter('path''|' $rootId '|'),
  159.             new RangeFilter('level', [
  160.                 RangeFilter::GT => $rootLevel,
  161.                 RangeFilter::LTE => $rootLevel $depth,
  162.             ])
  163.         );
  164.         $criteria->addAssociation('media');
  165.         $criteria->setLimit(null);
  166.         $criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_NONE);
  167.         /** @var CategoryCollection $levels */
  168.         $levels $this->categoryRepository->search($criteria$context)->getEntities();
  169.         return $levels;
  170.     }
  171.     private function getCategoryMetaInfo(string $activeIdstring $rootId): array
  172.     {
  173.         $result $this->connection->fetchAll('
  174.             # navigation-route::meta-information
  175.             SELECT LOWER(HEX(`id`)), `path`, `level`
  176.             FROM `category`
  177.             WHERE `id` = :activeId OR `parent_id` = :activeId OR `id` = :rootId
  178.         ', ['activeId' => Uuid::fromHexToBytes($activeId), 'rootId' => Uuid::fromHexToBytes($rootId)]);
  179.         if (!$result) {
  180.             throw new CategoryNotFoundException($activeId);
  181.         }
  182.         return FetchModeHelper::groupUnique($result);
  183.     }
  184.     private function getMetaInfoById(string $id, array $metaInfo): array
  185.     {
  186.         if (!\array_key_exists($id$metaInfo)) {
  187.             throw new CategoryNotFoundException($id);
  188.         }
  189.         return $metaInfo[$id];
  190.     }
  191.     private function loadChildren(string $activeIdSalesChannelContext $contextstring $rootId, array $metaInfoCategoryCollection $categoriesCriteria $criteria): CategoryCollection
  192.     {
  193.         $active $this->getMetaInfoById($activeId$metaInfo);
  194.         unset($metaInfo[$rootId], $metaInfo[$activeId]);
  195.         $childIds array_keys($metaInfo);
  196.         // Fetch all parents and first-level children of the active category, if they're not already fetched
  197.         $missing $this->getMissingIds($activeId$active['path'], $childIds$categories);
  198.         if (empty($missing)) {
  199.             return $categories;
  200.         }
  201.         $categories->merge(
  202.             $this->loadCategories($missing$context$criteria)
  203.         );
  204.         return $categories;
  205.     }
  206.     private function getMissingIds(string $activeId, ?string $path, array $childIdsCategoryCollection $alreadyLoaded): array
  207.     {
  208.         $parentIds array_filter(explode('|'$path ?? ''));
  209.         $haveToBeIncluded array_merge($childIds$parentIds, [$activeId]);
  210.         $included $alreadyLoaded->getIds();
  211.         $included array_flip($included);
  212.         return array_diff($haveToBeIncluded$included);
  213.     }
  214.     private function validate(string $activeId, ?string $pathSalesChannelContext $context): void
  215.     {
  216.         $ids array_filter([
  217.             $context->getSalesChannel()->getFooterCategoryId(),
  218.             $context->getSalesChannel()->getServiceCategoryId(),
  219.             $context->getSalesChannel()->getNavigationCategoryId(),
  220.         ]);
  221.         foreach ($ids as $id) {
  222.             if ($this->isChildCategory($activeId$path$id)) {
  223.                 return;
  224.             }
  225.         }
  226.         throw new CategoryNotFoundException($activeId);
  227.     }
  228.     private function isChildCategory(string $activeId, ?string $pathstring $rootId): bool
  229.     {
  230.         if ($rootId === $activeId) {
  231.             return true;
  232.         }
  233.         if ($path === null) {
  234.             return false;
  235.         }
  236.         if (mb_strpos($path'|' $rootId '|') !== false) {
  237.             return true;
  238.         }
  239.         return false;
  240.     }
  241.     private function resolveAliasId(string $idSalesChannelEntity $salesChannelEntity): ?string
  242.     {
  243.         switch ($id) {
  244.             case 'main-navigation':
  245.                 return $salesChannelEntity->getNavigationCategoryId();
  246.             case 'service-navigation':
  247.                 return $salesChannelEntity->getServiceCategoryId();
  248.             case 'footer-navigation':
  249.                 return $salesChannelEntity->getFooterCategoryId();
  250.             default:
  251.                 return $id;
  252.         }
  253.     }
  254. }