Skip to content

Instantly share code, notes, and snippets.

@ROBJkE
Created February 19, 2021 14:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ROBJkE/186c5ca28f5488388447ba673c1b8264 to your computer and use it in GitHub Desktop.
Save ROBJkE/186c5ca28f5488388447ba673c1b8264 to your computer and use it in GitHub Desktop.
NavigationRoute Decorator
<?php
namespace MyPlugin\Descorators;
use Shopware\Core\Content\Category\SalesChannel\AbstractNavigationRoute;
use Shopware\Core\Content\Category\SalesChannel\NavigationRoute;
use Shopware\Core\Content\Category\SalesChannel\NavigationRouteResponse;
use Doctrine\DBAL\Connection;
use OpenApi\Annotations as OA;
use Shopware\Core\Content\Category\CategoryCollection;
use Shopware\Core\Content\Category\Exception\CategoryNotFoundException;
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\RequestCriteriaBuilder;
use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
use Shopware\Core\Framework\Routing\Annotation\Entity;
use Shopware\Core\Framework\Routing\Annotation\RouteScope;
use Shopware\Core\Framework\Routing\Annotation\Since;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SalesChannel\SalesChannelEntity;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* @RouteScope(scopes={"store-api"})
*/
class NavigationRouteDecorator extends NavigationRoute
{
/**
* @var Connection
*/
private $connection;
/**
* @var SalesChannelRepositoryInterface
*/
private $categoryRepository;
private $decoratedService;
public function __construct(
Connection $connection,
SalesChannelRepositoryInterface $categoryRepository,
$myService
)
{
$this->connection = $connection;
$this->categoryRepository = $categoryRepository;
$this->decoratedService = $myService;
}
public function getDecorated(): AbstractNavigationRoute
{
throw new DecorationPatternException(self::class);
}
/**
* @Since("6.2.0.0")
* @Entity("category")
* @OA\Post(
* path="/navigation/{requestActiveId}/{requestRootId}",
* summary="Loads all available navigations",
* operationId="readNavigation",
* tags={"Store API", "Navigation"},
* @OA\Parameter(name="Api-Basic-Parameters"),
* @OA\Parameter(name="requestActiveId", description="Active Category ID", @OA\Schema(type="string"), in="path", required=true),
* @OA\Parameter(name="requestRootId", description="Root Category ID", @OA\Schema(type="string"), in="path", required=true),
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* @OA\Property(property="buildTree", description="Build category tree", type="boolean")
* )
* ),
* @OA\Response(
* response="200",
* description="All available navigations",
* @OA\JsonContent(ref="#/components/schemas/NavigationRouteResponse")
* )
* )
* @Route("/store-api/navigation/{requestActiveId}/{requestRootId}", name="store-api.navigation", methods={"GET", "POST"})
*/
public function load(
string $requestActiveId,
string $requestRootId,
Request $request,
SalesChannelContext $context,
?Criteria $criteria = null
): NavigationRouteResponse
{
$buildTree = $request->query->getBoolean('buildTree', $request->request->getBoolean('buildTree', true));
$depth = $request->query->getInt('depth', $request->request->getInt('depth', 2));
$activeId = $this->resolveAliasId($requestActiveId, $context->getSalesChannel());
$rootId = $this->resolveAliasId($requestRootId, $context->getSalesChannel());
if ($activeId === null) {
throw new CategoryNotFoundException($requestActiveId);
}
if ($rootId === null) {
throw new CategoryNotFoundException($requestRootId);
}
$metaInfo = $this->getCategoryMetaInfo($activeId, $rootId);
$active = $this->getMetaInfoById($activeId, $metaInfo);
$root = $this->getMetaInfoById($rootId, $metaInfo);
$isChild = $this->isChildCategory($activeId, $active['path'], $rootId);
// If the provided activeId is not part of the rootId, a fallback to the rootId must be made here.
// The passed activeId is therefore part of another navigation and must therefore not be loaded.
// The availability validation has already been done in the `validate` function.
if (!$isChild) {
$activeId = $rootId;
}
// @deprecated tag:v6.4.0 - Criteria will be required
if (!$criteria) {
$criteria = $this->requestCriteriaBuilder->handleRequest($request, new Criteria(), $this->categoryDefinition, $context->getContext());
}
// Load the first two levels without using the activeId in the query, so this can be cached
$categories = $this->loadLevels($rootId, (int) $root['level'], $context, clone $criteria, $depth);
// If the active category is part of the provided root id, we have to load the children and the parents of the active id
$categories = $this->loadChildren($activeId, $context, $rootId, $metaInfo, $categories, clone $criteria);
if ($buildTree) {
$categories = $this->buildTree($rootId, $categories->getElements());
}
return new NavigationRouteResponse($categories);
}
private function buildTree(?string $parentId, array $categories): CategoryCollection
{
$children = new CategoryCollection();
foreach ($categories as $key => $category) {
if ($category->getParentId() !== $parentId) {
continue;
}
unset($categories[$key]);
$children->add($category);
}
$children->sortByPosition();
$items = new CategoryCollection();
foreach ($children as $child) {
if (!$child->getActive() || !$child->getVisible()) {
continue;
}
$child->setChildren($this->buildTree($child->getId(), $categories));
$items->add($child);
}
return $items;
}
private function loadCategories(array $ids, SalesChannelContext $context, Criteria $criteria): CategoryCollection
{
$criteria->setIds($ids);
$criteria->addAssociation('media');
$criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_NONE);
/** @var CategoryCollection $missing */
$missing = $this->categoryRepository->search($criteria, $context)->getEntities();
return $missing;
}
private function loadLevels(string $rootId, int $rootLevel, SalesChannelContext $context, Criteria $criteria, int $depth = 2): CategoryCollection
{
$criteria->addFilter(
new ContainsFilter('path', '|' . $rootId . '|'),
new RangeFilter('level', [
RangeFilter::GT => $rootLevel,
RangeFilter::LTE => $rootLevel + $depth,
])
);
$criteria->addAssociation('media');
$criteria->setLimit(null);
$criteria->setTotalCountMode(Criteria::TOTAL_COUNT_MODE_NONE);
/** @var CategoryCollection $levels */
$levels = $this->categoryRepository->search($criteria, $context)->getEntities();
return $levels;
}
private function getCategoryMetaInfo(string $activeId, string $rootId): array
{
$result = $this->connection->fetchAll('
# navigation-route::meta-information
SELECT LOWER(HEX(`id`)), `path`, `level`
FROM `category`
WHERE `id` = :activeId OR `parent_id` = :activeId OR `id` = :rootId
', ['activeId' => Uuid::fromHexToBytes($activeId), 'rootId' => Uuid::fromHexToBytes($rootId)]);
if (!$result) {
throw new CategoryNotFoundException($activeId);
}
return FetchModeHelper::groupUnique($result);
}
private function getMetaInfoById(string $id, array $metaInfo): array
{
if (!\array_key_exists($id, $metaInfo)) {
throw new CategoryNotFoundException($id);
}
return $metaInfo[$id];
}
private function loadChildren(string $activeId, SalesChannelContext $context, string $rootId, array $metaInfo, CategoryCollection $categories, Criteria $criteria): CategoryCollection
{
$active = $this->getMetaInfoById($activeId, $metaInfo);
unset($metaInfo[$rootId], $metaInfo[$activeId]);
$childIds = array_keys($metaInfo);
// Fetch all parents and first-level children of the active category, if they're not already fetched
$missing = $this->getMissingIds($activeId, $active['path'], $childIds, $categories);
if (empty($missing)) {
return $categories;
}
$categories->merge(
$this->loadCategories($missing, $context, $criteria)
);
return $categories;
}
private function getMissingIds(string $activeId, ?string $path, array $childIds, CategoryCollection $alreadyLoaded): array
{
$parentIds = array_filter(explode('|', $path ?? ''));
$haveToBeIncluded = array_merge($childIds, $parentIds, [$activeId]);
$included = $alreadyLoaded->getIds();
$included = array_flip($included);
return array_diff($haveToBeIncluded, $included);
}
private function isChildCategory(string $activeId, ?string $path, string $rootId): bool
{
if ($rootId === $activeId) {
return true;
}
if ($path === null) {
return false;
}
if (mb_strpos($path, '|' . $rootId . '|') !== false) {
return true;
}
return false;
}
private function resolveAliasId(string $id, SalesChannelEntity $salesChannelEntity): ?string
{
switch ($id) {
case 'main-navigation':
return $salesChannelEntity->getNavigationCategoryId();
case 'service-navigation':
return $salesChannelEntity->getServiceCategoryId();
case 'footer-navigation':
return $salesChannelEntity->getFooterCategoryId();
default:
return $id;
}
}
}
<!-- Descorators -->
<service id="MyPlugin\Descorators\NavigationRouteDecorator" decorates="Shopware\Core\Content\Category\SalesChannel\NavigationRoute">
<argument type="service" id="Doctrine\DBAL\Connection"/>
<argument type="service" id="sales_channel.category.repository"/>
<argument type="service" id="MyPlugin\Descorators\NavigationRouteDecorator.inner" />
</service>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment