Search code examples
shopwareshopware6

How to show additional menu trees in Shopware 6 Storefront?


I try to add additional menues to the storefront.

We have a three column footer menu with links sorted in three topics.

  • links to legal pages
  • links to customer support topics
  • about us kind of information

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?


Solution

  • 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;
    }