I try to add additional menues to the storefront.
We have a three column footer menu with links sorted in three topics.
I now want to build a dropdown menu for the page header, that will show only two of the three categories and should be two levels deep.
I can not use the footer service navigation for this because it is hard coded to only show one level of navigation and I already use this navigation for another menu.
So I created a additional top-level-category tree that will only contain child categories with internal links to entries in the footer navigation.
What I have done so far:
I register a service in my theme
<services>
<service id="MyExample\Subscriber\HeaderPageletLoadedSubscriber">
<argument type="service" id="Shopware\Core\System\SystemConfig\SystemConfigService"/>
<argument type="service" id="Shopware\Core\Content\Category\Service\NavigationLoader"/>
<tag name="kernel.event_subscriber"/>
</service>
</services>
Then I create a Subscriber that registers on HeaderPageletLoadedEvent::class
and loads a category that I have configured in my themes plugin options.
<?php declare(strict_types=1);
namespace MyExample\Subscriber;
use Shopware\Core\Content\Category\Service\NavigationLoaderInterface;
use Shopware\Core\Content\Category\Tree\Tree;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Storefront\Pagelet\Header\HeaderPageletLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class HeaderPageletLoadedSubscriber implements EventSubscriberInterface
{
private NavigationLoaderInterface $navigationLoader;
private SystemConfigService $systemConfigService;
public function __construct(
SystemConfigService $systemConfigService,
NavigationLoaderInterface $navigationLoader
)
{
$this->systemConfigService = $systemConfigService;
$this->navigationLoader = $navigationLoader;
}
public static function getSubscribedEvents(): array
{
// Return the events to listen to as array like this: <event to listen to> => <method to execute>
return [
HeaderPageletLoadedEvent::class => 'onHeaderPageletLoaded'
];
}
public function onHeaderPageletLoaded(HeaderPageletLoadedEvent $event)
{
$context = $event->getSalesChannelContext();
$request = $event->getRequest();
$serviceTree = $this->getMenuTreeByKey($context, $request);
if ( $serviceTree ) {
$event->getPagelet()->addExtension('MyMenu', $serviceTree);
}
}
/**
* @param SalesChannelContext $salesChannelContext
* @param $request
* @return Tree
*/
private function getMenuTreeByKey(SalesChannelContext $salesChannelContext, $request): Tree
{
$salesChannelId = $salesChannelContext->getSalesChannelId();
$channelId = $this->systemConfigService->get('MyExample.config.serviceRoot', $salesChannelId);
$tree = null;
if ($channelId) {
$navigationId = $request->get('navigationId', $channelId);
$tree = $this->navigationLoader->load($navigationId, $salesChannelContext, $channelId, 2);
}
return $tree;
}
}
My code only works as long as I choose a category, that is also set as an Sales-Channel entry point in the Shopware 6 admin. If I select a category outside the entry-points I get a CategoryNotFoundException
This seems to be the same issue like reported on the Shopware forums
What is the proper way to load a category tree for the storefront, that will not be blocked by the validate()
function in /src/Core/Content/Category/SalesChannel/NavigationRoute.php?
The loader checks if the category either is one of the current sales channels entry points or one of their descendants. This is intended, as any categories outside of entry point trees are out of scope of the current sales channel. Even if you managed to get their respective trees, the out-of-scope categories wouldn't be accessible.
Was the reason you didn't set up the category as one of the descendants of the entry point because you didn't want it to show up in the main navigation? You could work around that by first setting the category to be hidden in the navigation.
However this will also cause the category no longer be part of the tree when you retrieve it. Instead of injecting the NavigationLoaderInterface
you can inject NavigationRoute
instead. You'll also need category.repository
to fetch the root category if you not only need its children.
<service id="MyExample\Subscriber\HeaderPageletLoadedSubscriber">
<argument type="service" id="category.repository"/>
<argument type="service" id="Shopware\Core\System\SystemConfig\SystemConfigService"/>
<argument type="service" id="Shopware\Core\Content\Category\SalesChannel\NavigationRoute"/>
</service>
Then you can use the route to get all the categories descending from your custom root id. You also need to set the request param buildTree
to true
so you get a category collection with the children being available up to the set level.
private function getMenuTreeByKey(SalesChannelContext $salesChannelContext, $request): ?CategoryCollection
{
$salesChannelId = $salesChannelContext->getSalesChannelId();
$channelId = $this->systemConfigService->get('MyExample.config.serviceRoot', $salesChannelId);
$tree = null;
if ($channelId) {
$navigationId = $request->get('navigationId', $channelId);
$request->query->set('buildTree', 'true');
$request->query->set('depth', '2');
$navigationId = $request->get('navigationId', $channelId);
/** @var CategoryEntity $category */
$category = $this->repository->search(new Criteria([$navigationId]), $salesChannelContext->getContext())->first();
$tree = $this->navigationRoute
->load($navigationId, $navigationId, $request, $salesChannelContext, new Criteria())
->getCategories();
$tree = $category->setChildren($tree);
}
return $tree;
}