[Edited] I’ve updated the article to do this in a better way

Recently I had to do something I thought it was simple to do: add the category images to the navigation menu. However, Magento doesn’t use a template to draw the whole menu but a function that will retrieve all categories and generate the ul/li tree.

In Magento 2.1.X you can do this rewriting the _getHtml() function from the class Magento\Theme\Block\Html\Topmenu. This is a function that calls itself once and again. We’ll create an observer that will add our code to the navigation menu.

1. Create your module

Create a foder in app/code/Vendor/NavigationMenu, where ‘vendor’ is your namespace for your modules.
app/code/Vendor/NavigationMenu/etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Vendor_NavigationMenu" setup_version="0.0.1"/></config>

Now the module registration:

<?php
/**
 * Registration file
 *
 * @category  Vendor
 * @package   Vendor\NavigationMenu
 * @author    Your Name <your@name.com>
 * @copyright 2017 Vendor
 * @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Vendor_NavigationMenu',
    __DIR__
);

 

2. Rewrite the class Magento\Theme\Block\Html\TopMenu

Create the di.xml to declare the rewriting of the class where the _getHtml() method is called:
app/code/Vendor/NavigationMenu/etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Magento\Theme\Block\Html\Topmenu" type="Vendor\NavigationMenu\Rewrite\Block\Html\Topmenu" />
</config>

Now you can create the class that will rewrite the method. Now the following points:

  • We will add a custom event and an observer to that class. That way we can easily plug whatever we need to the menu.
  • The actual image addition will be done in the observer.
  • We had to add some warning supressions, because the _getHtml() Magento native function is not compliant to some coding standards

app/code/Vendor/NavigationMenu/Rewrite/Block/Html/Topmenu.php

<?php
/**
 * Vendor Project
 * Module Vendor/NavigationMenu
 *
 * @category  Vendor
 * @package   Vendor\NavigationMenu
 * @author    Your Name <your.name@email.com>
 * @copyright 2017 Vendor
 * @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */
namespace Vendor\NavigationMenu\Rewrite\Block\Html;

use Magento\Framework\Data\Tree\Node;
use Magento\Framework\DataObject;
use Magento\Framework\View\Element\Template;

/**
 * Plugin NavigationMenu
 *
 * @author    Your Name <your.name@email.com>
 * @copyright 2017 Vendor
 */
class Topmenu extends \Magento\Theme\Block\Html\Topmenu
{
    /**
     * Recursively generates top menu html from data that is specified in $menuTree
     *
     * @param Node   $menuTree          menu tree
     * @param string $childrenWrapClass children wrap class
     * @param int    $limit             limit
     * @param array  $colBrakes         column brakes
     * @return string
     *
     * @SuppressWarnings(PHPMD)
     */
    protected function _getHtml(
        Node $menuTree,
        $childrenWrapClass,
        $limit,
        $colBrakes = []
    ) {
        $html = parent::_getHtml($menuTree, $childrenWrapClass, $limit, $colBrakes = []);

        $transportObject = new DataObject(['html' => $html, 'menu_tree' => $menuTree]);
        $this->_eventManager->dispatch(
            'vendor_topmenu_node_gethtml_after',
            ['menu' => $this->_menu, 'transport' => $transportObject]
        );

        $html = $transportObject->getHtml();

        return $html;
    }
}

3. Create an observer triggered by the event

For this, we’ll need to create the events.xml configuration file and assign the class that will handle the event:

app/code/Vendor/NavigationMenu/etc/frontend/events.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="vendor_topmenu_node_gethtml_after">
        <observer name="vendor_navigationmenu_topmenu" instance="Vendor\NavigationMenu\Observer\AddContentToCategoryTopmenu" />
    </event>
</config>

app/code/Vendor/NavigationMenu/Observer/AddContentToCategoryTopmenu.php

<?php
/**
 * Topmenu catalog observer to add custom additional elements
 *
 * @category  Vendor
 * @package   Vendor\NavigationMenu
 * @author    Your Name <your.name@email.com>
 * @copyright 2017 Vendor
 * @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */
namespace Vendor\NavigationMenu\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Catalog\Api\CategoryRepositoryInterface;

/**
 * Class AddFirstCategoryImageToTopmenu
 * @package Vendor\NavigationMenu
 */
class AddContentToCategoryTopmenu implements ObserverInterface
{
    /**
     * @var CategoryRepositoryInterface $categoryRepository
     */
    protected $categoryRepository;

    /**
     * AddFirstCategoryImageToTopmenu constructor.
     *
     * @param CategoryRepositoryInterface $categoryRepository repository
     */
    public function __construct(
        CategoryRepositoryInterface $categoryRepository
    ) {
        $this->categoryRepository = $categoryRepository;
    }

    /**
     * @param Observer $observer Observer object
     */
    public function execute(Observer $observer)
    {
        $transport = $observer->getTransport();
        $html      = $transport->getHtml();
        $menuTree  = $transport->getMenuTree();

        $parentLevel = $menuTree->getLevel();
        $childLevel = $parentLevel === null ? 0 : $parentLevel + 1;

        $menuId = $menuTree->getId();

        if ($childLevel == 1 && $this->isCategory($menuId)) {
            $html .= '<li class="category_image" style=""><img src="'.$this->getCategoryImage($menuId).'"/></li>';
        }

        $transport->setHtml($html);
    }

    /**
     * Retrieves the category image for the corresponding child
     *
     * @param string $categoryId Category composed ID
     *
     * @return string
     */
    protected function getCategoryImage($categoryId)
    {
        $categoryIdElements = explode('-', $categoryId);
        $category           = $this->categoryRepository->get(end($categoryIdElements));
        $categoryName       = $category->getImageUrl();

        return $categoryName;
    }

    /**
     * Check if current menu element corresponds to a category
     *
     * @param string $menuId Menu element composed ID
     *
     * @return string
     */
    protected function isCategory($menuId)
    {
        $menuId = explode('-', $menuId);

        return 'category' == array_shift($menuId);
    }
}

7 Comments

Sandro · March 27, 2017 at 11:06 am

Hi Pau,

I have done everything according to your post, yet my Navbar disappears completely, instead of displaying the category images..
Any thoughts?
Thanks for your efforts

    Pau · April 22, 2017 at 3:33 am

    Hi Sandro,

    I need to know more about the problem you’re having. Actually, as you can see in my post, we only add the and extra html element to the navigation menu. If you don’t see that extra element and your menu stops working, it might simply means an error in your code. The rewrite I propose in this post is quite simple, so check the Magento logs or apache logs to see where the error is.

Franco · May 5, 2017 at 3:35 pm

I’m experiencing the same issue as Sandro ! :S Any clues?

Tri · June 6, 2017 at 2:53 am

I’m experiencing the same issue as Sandro ! :S Any clues? . can you help me ?

mattkrupnik · January 2, 2018 at 11:45 am

Hello,

Everything work fine Magento v.2.2.2

Cheers
Matt

John · April 12, 2018 at 3:44 pm

This is working I modified it a bit however when im adding image to 2 lvl categories so we have for example Jeans -> Pockets Pockets image does not show until you add a subcategory to Pockets. I was smashing my head around this for few hours now. I got that $menuId will have top lvl menu items but tbh getImageUrl should work on subcategories too. Does anybody have any idea bout this.

Frank · March 1, 2019 at 3:22 am

Hi, Thank you for posting this. It was really helpful.
Could you guide me on how to include the submenu in this? Right now it only works for level 1 and 2 menu and not submenu items generated in _addSubMenu function.
Thank You

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.