Question

I have created a Module which allows me to edit the HTML output of the top menu by extending the Topmenu block with my own custom one.

I know that this module works because I have used it on a different website before, however for some reason when I try to get it working on a fresh install it's not working correctly. I can't see where it's going wrong.

It appears like the module is not extending Magentos top menu class and therefor the topmenu functions are unavailable.

Error

Fatal error: Call to a member function setOutermostClass() on null in /var/www/buypartysuppliesliverpool.co.uk/app/code/Myvendor/Topmenu/Plugin/Topmenu.php on line 36

And the Topmenu.php file I am using is:

<?php
/**
 * Copyright © 2013-2017 Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Myvendor\Topmenu\Plugin;

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

/**
 * Html page top menu block
 */
class Topmenu extends \Magento\Theme\Block\Html\Topmenu
{


    /**
     * Get top menu html
     *
     * @param string $outermostClass
     * @param string $childrenWrapClass
     * @param int $limit
     * @return string
     */
    public function getHtml($outermostClass = '', $childrenWrapClass = '', $limit = 0)
    {
        $this->_eventManager->dispatch(
            'page_block_html_topmenu_gethtml_before',
            ['menu' => $this->_menu, 'block' => $this]
        );

        $this->_menu->setOutermostClass($outermostClass);
        $this->_menu->setChildrenWrapClass($childrenWrapClass);

        $html = $this->_getHtml($this->_menu, $childrenWrapClass, $limit);

        $transportObject = new \Magento\Framework\DataObject(['html' => $html]);
        $this->_eventManager->dispatch(
            'page_block_html_topmenu_gethtml_after',
            ['menu' => $this->_menu, 'transportObject' => $transportObject]
        );
        $html = $transportObject->getHtml();
        return $html;
    }

    /**
     * Count All Subnavigation Items
     *
     * @param \Magento\Backend\Model\Menu $items
     * @return int
     */
    protected function _countItems($items)
    {
        $total = $items->count();
        foreach ($items as $item) {
            /** @var $item \Magento\Backend\Model\Menu\Item */
            if ($item->hasChildren()) {
                $total += $this->_countItems($item->getChildren());
            }
        }
        return $total;
    }

    /**
     * Building Array with Column Brake Stops
     *
     * @param \Magento\Backend\Model\Menu $items
     * @param int $limit
     * @return array|void
     *
     * @todo: Add Depth Level limit, and better logic for columns
     */
    protected function _columnBrake($items, $limit)
    {
        $total = $this->_countItems($items);
        if ($total <= $limit) {
            return;
        }

        $result[] = ['total' => $total, 'max' => (int)ceil($total / ceil($total / $limit))];

        $count = 0;
        $firstCol = true;

        foreach ($items as $item) {
            $place = $this->_countItems($item->getChildren()) + 1;
            $count += $place;

            if ($place >= $limit) {
                $colbrake = !$firstCol;
                $count = 0;
            } elseif ($count >= $limit) {
                $colbrake = !$firstCol;
                $count = $place;
            } else {
                $colbrake = false;
            }

            $result[] = ['place' => $place, 'colbrake' => $colbrake];

            $firstCol = false;
        }

        return $result;
    }

    /**
     * Add sub menu HTML code for current menu item
     *
     * @param \Magento\Framework\Data\Tree\Node $child
     * @param string $childLevel
     * @param string $childrenWrapClass
     * @param int $limit
     * @return string HTML code
     */
    protected function _addSubMenu($child, $childLevel, $childrenWrapClass, $limit)
    {
        $html = '';
        if (!$child->hasChildren()) {
            return $html;
        }

        $colStops = null;
        if ($childLevel == 0 && $limit) {
            $colStops = $this->_columnBrake($child->getChildren(), $limit);
        }

        $html .= '<ul class="level' . $childLevel . ' megaSub">';
        $html .= $this->_getHtml($child, $childrenWrapClass, $limit, $colStops);
        $html .= '</ul>';

        return $html;
    }

    /**
     * Recursively generates top menu html from data that is specified in $menuTree
     *
     * @param \Magento\Framework\Data\Tree\Node $menuTree
     * @param string $childrenWrapClass
     * @param int $limit
     * @param array $colBrakes
     * @return string
     *
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     * @SuppressWarnings(PHPMD.NPathComplexity)
     */
    protected function _getHtml(
        \Magento\Framework\Data\Tree\Node $menuTree,
        $childrenWrapClass,
        $limit,
        $colBrakes = []
    ) {
        $html = '';

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

        $counter = 1;
        $itemPosition = 1;
        $childrenCount = $children->count();

        $parentPositionClass = $menuTree->getPositionClass();
        $itemPositionClassPrefix = $parentPositionClass ? $parentPositionClass . '-' : 'nav-';

        foreach ($children as $child) {
            $child->setLevel($childLevel);
            $child->setIsFirst($counter == 1);
            $child->setIsLast($counter == $childrenCount);
            $child->setPositionClass($itemPositionClassPrefix . $counter);

            $outermostClassCode = '';
            $outermostClass = $menuTree->getOutermostClass();

            if ($childLevel == 0 && $outermostClass) {
                $outermostClassCode = ' class="' . $outermostClass . '" ';
                $child->setClass($outermostClass);
            }

            if (count($colBrakes) && $colBrakes[$counter]['colbrake']) {
                // $html .= '</ul></li><li class="column"><ul>';
            }

            if($counter > 1 && $childLevel == 1){
                continue;
            }

            if($childLevel == 1){

                // Overall megasub li container
                $html .= '<li class="row">';
                // Sub catergories here
                $html .= '<div class="medium-3 columns">';
                $html .= '<h4>Sub Catergories</h4>';
                $html .= '<hr class="dashed">';
                $html .= '<ul>';

                // Inset sub categories here
                $html .= '<li ' . $this->_getRenderedMenuItemAttributes($child) . '>';
                $html .= '<a href="' . $child->getUrl() . '" ' . $outermostClassCode . '><span>' . $this->escapeHtml(
                    $child->getName()
                ) . '</span></a></li>';
                $itemPosition++;
                $counter++;
                // End of subcategory generation

                $html .= '</ul>';
                $html .= '</div>';
                // End of subcategories

                // Get Layered Navigation
                $currentId = $child->getId();

                $objectManager = \Magento\Framework\App\ObjectManager::getInstance();

                $filterableAttributes = $objectManager->get(\Magento\Catalog\Model\Layer\Category\FilterableAttributeList::class);

                $appState = $objectManager->get(\Magento\Framework\App\State::class);
                $layerResolver = $objectManager->get(\Magento\Catalog\Model\Layer\Resolver::class);
                $filterList = $objectManager->create(
                \Magento\Catalog\Model\Layer\FilterList::class,
                    [
                        'filterableAttributes' => $filterableAttributes
                    ]
                );
                error_log($currentId);
                $category = 4;

                // $appState->setAreaCode('frontend');
                $layer = $layerResolver->get();
                $layer->setCurrentCategory($category);
                $filters = $filterList->getFilters($layer);

                $finalFilters = [];
                foreach ($filters as $filter) {

                    // Ignore category and price
                    if(strtolower($filter->getName()) == "price" || strtolower($filter->getName()) == "category"){
                    // if(strtolower($filter->getName()) == "price"){
                        continue;
                    }

                    if ($filter->getItemsCount()) {
                        $finalFilters[] = $filter;

                        $name = $filter->getName();
                        $html .= '<div class="medium-3 columns">';
                        $html .= "<h4>Browse by ".$name."</h4>";
                        $html .= '<hr class="dashed">';
                        $html .= '<ul>';
                        foreach ($filter->getItems() as $item) {
                            $tmparray = explode("?", $item->getUrl());
                            $newurl = array_pop($tmparray);
                            $html .= "<li><a href='".$child->getUrl().'?'.$newurl."'>".$item->getLabel()."</a></li>";
                        }
                        $html .= '</ul>';
                        $html .= '</div>';
                    }
                }

                // End of Layered Navigation
                // End of megasub li container
                $html .= '</li>';

            }
            else {
                $html .= '<li ' . $this->_getRenderedMenuItemAttributes($child) . '>';
                $html .= '<a href="' . $child->getUrl() . '" ' . $outermostClassCode . '><span>' . $this->escapeHtml(
                    $child->getName()
                ) . '</span></a>' . $this->_addSubMenu(
                    $child,
                    $childLevel,
                    $childrenWrapClass,
                    $limit
                ) . '</li>';
                $itemPosition++;
                $counter++;                
            }
        }

        if (count($colBrakes) && $limit) {
            $html = '<li class="column"><ul>' . $html . '</ul></li>';
        }

        return $html;
    }

    /**
     * Generates string with all attributes that should be present in menu item element
     *
     * @param \Magento\Framework\Data\Tree\Node $item
     * @return string
     */
    protected function _getRenderedMenuItemAttributes(\Magento\Framework\Data\Tree\Node $item)
    {
        $html = '';
        $attributes = $this->_getMenuItemAttributes($item);
        foreach ($attributes as $attributeName => $attributeValue) {
            $html .= ' ' . $attributeName . '="' . str_replace('"', '\"', $attributeValue) . '"';
        }
        return $html;
    }

    /**
     * Returns array of menu item's attributes
     *
     * @param \Magento\Framework\Data\Tree\Node $item
     * @return array
     */
    protected function _getMenuItemAttributes(\Magento\Framework\Data\Tree\Node $item)
    {
        $menuItemClasses = $this->_getMenuItemClasses($item);
        return ['class' => implode(' ', $menuItemClasses)];
    }

    /**
     * Returns array of menu item's classes
     *
     * @param \Magento\Framework\Data\Tree\Node $item
     * @return array
     */
    protected function _getMenuItemClasses(\Magento\Framework\Data\Tree\Node $item)
    {
        $classes = [];

        $classes[] = 'level' . $item->getLevel();
        $classes[] = $item->getPositionClass();

        if ($item->getIsFirst()) {
            $classes[] = 'first';
        }

        if ($item->getIsActive()) {
            $classes[] = 'active';
        } elseif ($item->getHasActive()) {
            $classes[] = 'has-active';
        }

        if ($item->getIsLast()) {
            $classes[] = 'last';
        }

        if ($item->getClass()) {
            $classes[] = $item->getClass();
        }

        if ($item->hasChildren()) {
            $classes[] = 'parent';
        }

        return $classes;
    }

    /**
     * Add identity
     *
     * @param array $identity
     * @return void
     */
    public function addIdentity($identity)
    {
        if (!in_array($identity, $this->identities)) {
            $this->identities[] = $identity;
        }
    }

    /**
     * Get identities
     *
     * @return array
     */
    public function getIdentities()
    {
        return $this->identities;
    }

    /**
     * Get cache key informative items
     *
     * @return array
     */
    public function getCacheKeyInfo()
    {
        $keyInfo = parent::getCacheKeyInfo();
        $keyInfo[] = $this->getUrl('*/*/*', ['_current' => true, '_query' => '']);
        return $keyInfo;
    }

    /**
     * Get tags array for saving cache
     *
     * @return array
     */
    protected function getCacheTags()
    {
        return array_merge(parent::getCacheTags(), $this->getIdentities());
    }

    /**
     * Get menu object.
     *
     * @return Node
     */
    public function getMenu()
    {
        return $this->_menu;
    }
}

Any ideas why this is happening?

Thanks

Was it helpful?

Solution

This problem is a result of a recent Magento update which explains why I was having this problem using code that had previously worked for me on earlier Magento versions.

Final Solution The final solution to fix this after Magento's update is to replace $this->_menu with $this->getMenu()

Now the working version of my custom module extends the Magento top menu class and just overrides the _getHtml function to change the markup as required.

Licensed under: CC-BY-SA with attribution
Not affiliated with magento.stackexchange
scroll top