[Mini Tutorial] Add Children Categories as Top Navigation

Recently one of our clients came to us with an interesting request:

I want to be able to promote a subcategory to the main navigation, for promotional purposes, but I don’t want to move the category out of its parent.

What that means is, they wanted to take an existing sub-category and have it appear on our main-navigation tree without having to resort to using any dummy categories (making a faux category and having it redirect) or moving the sub-category out of its parent.

For example, let’s say we have a top category called Toys:

Screen Shot 2014-12-12 at 5.35.37 PM

Hypothetically speaking, let’s pretend Lego is having a sale on all their items (It’s always been my dream that this happens), and they want their menu item displayed across the top navigation bar for marketing purposes. We know that the top navigation bar drives traffic to its categories, so our client wants to make the most of it and sell as much Lego products as possible.

One solution is to simply create a top level category called Lego, and have it redirect.
For the context of our problem though, let’s pretend Lego is categorized into the following:

Screen Shot 2014-12-12 at 5.43.46 PM

So now what our client wants is to be able to retain that navigation tree without removing “Lego” from “Toys”. We can create children in our top dummy category, but then we’d have to setup an additional redirect for every child. That’s very costly in terms of manual labour and clicking, and we’d probably want to avoid doing this each time we have a promotion.

Our Magento friendly suggestion – Take the node of the sub-category “Lego” and clone it into the top navigation. Here’s how:

Related: [Mini Tutorial] Adding Custom Links with local.xml

Create your Module

Start by creating your own module. If you’re unsure how, Alan Storm here provides a great tutorial for doing so: The Magento Config

Create an Observer model in your module. Give it one main method that will hook into Magento’s event: page_block_html_topmenu_gethtml_before

I’m going to call it: addPromotedCategories()

//Remember to put in your own package / module names above!
class <Your_Package>_<Your_Module_Name>_Model_Observer
{
    public function addPromotedCategories($observer) {
    }

}

Now, add this section to your config.xml within the scope.

    ...
    <frontend>
        <events>
            <page_block_html_topmenu_gethtml_before>
                <observers>
                    <your_modulename>
                        <class>YourPackage_YourModule_Model_Observer</class>
                        <method>addPromotedCategories</method>
                    </your_modulename>
                </observers>
            </page_block_html_topmenu_gethtml_before>
        </events>
    </frontend>
    ...

We’re ready to inject some of our own nodes. Before we jump into that though, we need to give our backend users the ability to mark specific categories for promotions. Prepare your module with setup resources so that we may write an install script. For more information on how that’s done, please take a look at Alan Storm’s article: Setup Resources

In our install script, we’re going to add the following:

$installer = $this;
$installer->startSetup();

$setup = new Mage_Eav_Model_Entity_Setup('core_setup');

$setup->addAttribute(Mage_Catalog_Model_Category::ENTITY, 'promote_to_top', array(
    'group'         => 'General Information',
    'input'         => 'select',
    'type'          => 'int',
    'label'         => 'Promote To Top',
    'backend'       => '',
    'source'        => 'eav/entity_attribute_source_boolean',
    'visible'       => true,
    'required'      => false,
    'visible_on_front' => true,
    'global'        => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE,
));

$setup->endSetup();

$installer->endSetup();

This will create a new attribute on all our categories, allowing us to define which categories are to be duplicated and added to our top navigation. It should look like this, when viewing any category:

Screen Shot 2014-12-15 at 4.04.43 PM

For now, this attribute will do nothing. Let’s set Lego’s promote_to_top attribute as “Yes”, and start coding our Observer.

Related: Code Quality in the Magento Ecosystem

Function addPromotedCategories

The event observer contains 3 pieces of data, one of which is the Varien_Data_Tree_Node $menu object with our category tree in it. The other two objects are, the Varien_Event object, and the TopMenu block.
We’re interested in the tree.

...
    public function addPromotedCategories($observer) {
        $tree = $observer->getMenu();
    }
...

We also want to grab every category in Magento that we’re promoting.

...
    public function addPromotedCategories($observer) {
        $tree = $observer->getMenu();
        $topCategories = Mage::getModel('catalog/category')->getCollection()->addAttributeToSelect('*')->addAttributeToFilter('promote_to_top', '1');
    }
...

Breakdown of the call above:

Mage::getModel(‘catalog/category’) tells the Magento Class factory to instantiate an instance of Mage_Catalog_Model_Category for us.
->getCollection() tells Magento to instantiate a Collection of Categories
->addAttributeToSelect(‘*’) selects all attributes of the category
->addAttributeToFilter(‘promote_to_top’, ‘1’) where promote_to_top is set to “Yes”.

Now, for each of the categories we return from our call, we want to find the node in our existing tree so that we can duplicate it and add it to our main tree.

Start a foreach statement:

...
    public function addPromotedCategories($observer) {
        $tree = $observer->getMenu();
        $topCategories = Mage::getModel('catalog/category')->getCollection()->addAttributeToSelect('*')->addAttributeToFilter('promote_to_top', '1');

        foreach ($topCategories as $category) {
        }
    }
...

Lets:

  1. Find the node
  2. Add the Node to the tree

Each category node in Magento is named “category-node-“. That means if we search our tree for a node called “category-node-” + ID of our current category, we’ll be able to find out whether the node exists in the tree or not.

...
    public function addPromotedCategories($observer) {
        $tree = $observer->getMenu();
        $topCategories = Mage::getModel('catalog/category')->getCollection()->addAttributeToSelect('*')->addAttributeToFilter('promote_to_top', '1');

        foreach ($topCategories as $category) {
            $nodeName = "category-node-" . $category->getId();
            $treeNode = $this->_findNode($nodeName, $tree);

            if ($treeNode) {
                $tree->addChild($treeNode);
            }
        }
    }
...

You’ll notice I’ve made a call to $this->_findNode(), which hasn’t been defined yet.
This code actually derives itself from Varien’s own find node code, which simply gets all the child nodes of the tree and compares names.
The definition is as follows:

    public function _findNode($name, $tree) {
        $allNodes = $tree->getAllChildNodes();
        if (isset($allNodes[$name])) {
            return $allNodes[$name];
        }
        return null; //Returns nothing otherwise
    }

Add the function above to your observer.

Now if you look at the frontend, you should see the categories look like this:

Screen Shot 2014-12-15 at 4.36.55 PM

Notice we still have all our children categories added the same way as a normal top level category?
Because the nodes are the same as the ones found here:

Screen Shot 2014-12-15 at 4.43.23 PM

Both of these navigation menus link to the same URL Rewrites. No need to add custom rewrites!

Closing Notes

It’s rather unsightly though to constantly pad categories to the end of the navigational bar. For my next post, I’ll write a tutorial on how to inject one or more promoted categories into any position on your top navigation menu. As a challenge, how about seeing if you can write it yourself?

Hint:
Remember to sort your injected items BEFORE adding them to the tree!

Good luck!

Related: [Mini Tutorial] – Extend the Magento API