cancel
Showing results for 
Search instead for 
Did you mean: 

Magento 2 has no ability to order an event's observers execution

Magento 2 has no ability to order an event's observers execution

Feature request from mage2pro, posted on GitHub Nov 13, 2015

https://mage2.pro/t/200

For example, if there are mutiple observers for the catalog_block_product_status_display event then the Magento 2 behavior becomes unpredictable because we can not set the observer's ordering.

9 Comments
apiuser
New Member

Comment from rhamoudi, posted on GitHub Dec 02, 2015

Have you tested that the ordering isn't done by reading the xml from top to bottom?

apiuser
New Member

Comment from Yonn-Trimoreau, posted on GitHub Feb 17, 2016

+1

It will be really useful for ordering menu items when observing on page_block_html_topmenu_gethtml_before event.

apiuser
New Member

Comment from ntzz, posted on GitHub May 17, 2016

@Yonn-Trimoreau Is possible to order top menu yet?

apiuser
New Member

Comment from Yonn-Trimoreau, posted on GitHub May 20, 2016

Yes, you can plug an observer to the event "page_block_html_topmenu_gethtml_before" and recreate your own nodes. That's the only way I found.

In file Vendor/ModuleName/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="page_block_html_topmenu_gethtml_before">
        <observer name="add_items_to_menu" instance="Vendor\ModuleName\Observer\AddItemsToMenu"/>
    </event>
</config>

In file Vendor/ModuleName/Observer/AddItemsToMenu.php :

<?php
namespace Vendor\ModuleName\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;

class AddItemsToMenu implements ObserverInterface
{
    /**
     * @var \Magento\Store\Model\StoreManagerInterface
     */
    protected $_storeManager;

    /**
     * @var \Magento\Framework\UrlInterface
     */
    protected $_urlInterface;

    /**
     * AddItemsToMenu Observer constructor.
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     * @param \Magento\Framework\UrlInterface $urlInterface
     */
    public function __construct(
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Framework\UrlInterface $urlInterface
    ) {
        $this->_storeManager = $storeManager;
        $this->_urlInterface = $urlInterface;
    }

    /**
     * @param Observer $observer
     * @return void
     */
    public function execute(\Magento\Framework\Event\Observer $observer)
    {
        /** @var \Magento\Framework\Data\Tree\Node $parentCategoryNode */
        $parentCategoryNode = $observer->getMenu();
        $block = $observer->getEvent()->getBlock();
        $tree = $parentCategoryNode->getTree();
        $currentUrl = $this->_urlInterface->getCurrentUrl();

        // Create menu array
        $nodesData = array(
            array(
                "name" => "Home",
                "id" => "home",
                "url" => $url = $block->getUrl(''),
                "has_active" => false,
                "is_active" => $currentUrl == $url ? true : false,
                "extra_class" => "home",
                "order" => 1,
            ),
            array(
                "name" => "Menu1",
                "id" => "menu1",
                "url" => $url = $block->getUrl('url/menu1'),
                "has_active" => false,
                "is_active" => $currentUrl == $url ? true : false,
                "extra_class" => "menu1",
                "order" => 2,
            ),
            [...]
        );

        foreach ($nodesData as $nodeData) {
            $categoryNode = new \Magento\Framework\Data\Tree\Node($nodeData, 'id', $tree, $parentCategoryNode);
            $parentCategoryNode->addChild($categoryNode);
        }
    }
}
apiuser
New Member

Comment from BarryCarlyon, posted on GitHub Jun 17, 2016

As far as I can tell, this will only add items onto the end of the menu.

I am as yet to find a way to add items to the start of the menu.

(But using the same "name":

<observer name="catalog_add_topmenu_items" instance="Magento\Catalog\Observer\AddCatalogToTopmenuItemsObserver" />

so

<observer name="catalog_add_topmenu_items" instance="Vendor\ModuleName\Observer\AddItemsToMenu"/

Instead

Means you can override it it seems)

apiuser
New Member

Comment from BarryCarlyon, posted on GitHub Jun 17, 2016

But really an

$parentCategoryNode->prependChild($categoryNode);

Would be more useful

apiuser
New Member

Comment from BarryCarlyon, posted on GitHub Jun 17, 2016

I've ended up with:

etc/frontend/events.xml

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="page_block_html_topmenu_gethtml_before">
        <observer name="catalog_add_topmenu_items" instance="Vendor\Module\Observer\TopMenu" />
    </event>
</config>

Observer/TopMenu.php

<?php

namespace Vendor\Module\Observer;

use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Data\Tree\Node;

class Topmenu implements ObserverInterface {
    /**
     * @var \Psr\Log\LoggerInterface
     */
    protected $logger;

    /**
     * @param \Magento\Catalog\Observer\AddCatalogToTopmenuItemsObserver $orig
     * @param \Psr\Log\LoggerInterface $logger
     */
    public function __construct(
        \Magento\Catalog\Observer\AddCatalogToTopmenuItemsObserver $orig,
        \Psr\Log\LoggerInterface $logger
    ) {
        $this->logger = $logger;

        $this->orig = $orig;
    }

    /**
     * Apply customized static files to frontend
     *
     * @param \Magento\Framework\Event\Observer $observer
     * @return void
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function execute(\Magento\Framework\Event\Observer $observer)
    {
        $menu = $observer->getMenu();
        $tree = $menu->getTree();

        $data = [
            'name'      => 'TestING',
            'id'        => 'test',
            'url'       => '/test/',
            'is_active' => false
        ];

        $node = new Node($data, 'id', $tree, $menu);
        $menu->addChild($node);

        // add cats by calling the original observer
        $this->orig->execute($observer);
    }
}
apiuser
New Member

Comment from Yonn-Trimoreau, posted on GitHub Jun 21, 2016

My solution rebuilds the tree entirely, it does not only add nodes to the end Smiley Wink

bobspongieux
Senior Member

Forgive me, I was wrong, my first solution wasn't rebuilding the entire tree.

 

Here is the solution to add ordering functionality to your menu.

 

In app/code/Company/ModuleName/etc/frontend/di.xml :

 

<preference for="Magento\Theme\Block\Html\Topmenu" type="Company\ModuleName\Block\Html\OrderedTopmenu"/>

In app/code/Company/ModuleName/Block/Html/OrderedTopMenu.php :

 

<?php

namespace Company\ModuleName\Block\Html;

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

//
// FIXME: Menu ordering and CSS classes customization is home-made
//
//      See: https://github.com/magento/magento2/issues/2354
//      https://github.com/magento/magento2/issues/3446
//
class OrderedTopmenu extends \Magento\Theme\Block\Html\Topmenu
{
    /**
     * Custom version that handles an 'order' attribute. Defaults position is 100 if not specified.
     *
     * @param \Magento\Framework\Data\Tree\Node $menuTree
     * @param string $childrenWrapClass
     * @param int $limit
     * @param array $colBrakes
     * @return string
     */
    protected function _getHtml(
        \Magento\Framework\Data\Tree\Node $menuTree,
        $childrenWrapClass,
        $limit,
        $colBrakes = []
    ) {
        $children = [];

        foreach ($menuTree->getChildren() as $node) {
            $children[] = $node;
            $menuTree->getChildren()->delete($node);
        }

        usort($children, function($a, $b) {
            $i = $a->getOrder() ?: 100;
            $j = $b->getOrder() ?: 100;

            return $i - $j;
        });

        for ($i = 0; $i < count($children); $i++) {
            $menuTree->getChildren()->add($children[$i]);
        }

        return parent::_getHtml($menuTree, $childrenWrapClass, $limit, $colBrakes);
    }

    /**
     * Custom version to handle an 'extra_class' attribute.
     *
     * @see \Magento\Theme\Block\Html\Topmenu::_getMenuItemClasses()
     */
    protected function _getMenuItemClasses(\Magento\Framework\Data\Tree\Node $item)
    {
        $classes = parent::_getMenuItemClasses($item);

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

        return $classes;
    }
}

In Company/ModuleName/etc/frontend/events.xml :

 

<event name="page_block_html_topmenu_gethtml_before">
    <observer name="company_modulename_add_items_to_menu" instance="Company\ModuleName\Observer\AddItemsToMenu"/>
</event>

In Company/ModuleName/Observer/AddItemsToMenu.php :

 

<?php

namespace Company\ModuleName\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;

class AddItemsToMenu implements ObserverInterface
{
/**
* @var \Magento\Store\Model\StoreManagerInterface $_storeManager
*/
protected $_storeManager;

/**
* @var \Magento\Framework\UrlInterface $_urlInterface
*/
protected $_urlInterface;

/**
* ForceLogin Observer constructor.
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param \Magento\Framework\UrlInterface $urlInterface
*/
public function __construct(
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Framework\UrlInterface $urlInterface
) {
$this->_storeManager = $storeManager;
$this->_urlInterface = $urlInterface;
}

/**
* @param Observer $observer
* @return void
*/
public function execute(\Magento\Framework\Event\Observer $observer)
{
/** @var \Magento\Framework\Data\Tree\Node $parentCategoryNode */
$parentCategoryNode = $observer->getMenu();
$block = $observer->getEvent()->getBlock();
$tree = $parentCategoryNode->getTree();
$currentUrl = $this->_urlInterface->getCurrentUrl();

// Create menu array
$nodesData = array(
array(
"name" => __('Item name'),
"id" => "item-id",
"url" => $url = rtrim($block->getUrl('company_modulename/url'), '/'),
"has_active" => false, // If one of his children is activated
"is_active" => $currentUrl == $url ? true : false,
"extra_class" => "extra-class",
"order" => 110
),
...
...
...

);

foreach ($nodesData as $nodeData) {
$categoryNode = new \Magento\Framework\Data\Tree\Node($nodeData, 'id', $tree, $parentCategoryNode);
$parentCategoryNode->addChild($categoryNode);
}
}
}

In Company/ModuleName/view/frontend/layout/default.xml :

 

<referenceBlock name="catalog.topnav">
<action method="setTemplate"> <argument name="template" xsi:type="string">Magento_Theme::html/topmenu.phtml</argument> </action> </referenceBlock>

 

Smiley Wink