Categories


Archives


Recent Posts


Categories


Building the Pulse Storm Launcher: Menu Data

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

This entry is part 2 of 2 in the series Magento Pulse Storm Launcher. Earlier posts include Fast and Painless Magento Admin Navigation. This is the most recent post in the series.

Thanks to everyone who downloaded and re-tweeted my announcement of the Magento Pulse Storm launcher. There were a few bugs related to style in Magento 1.6, as well as a few related to existing Magento admin ajax pages. These bugs have been fixed, so grab a new copy from GitHub or the local tarball. I’m particularly interested in feedback/bug-reports if you’re running a pre-1.6 version of Magento, or are running the Magento admin in a non-US_EN locale. The current extension also pulls in information from Magento’s Global Search, so comments on this new functionality are also welcome.

The next few articles here, including this one, are going to focus on how I built the launcher. While I won’t cover every single line of code, I will focus on the high level issues I encountered, as well as my general methodology for solving Magento related problems.

Starting Out

My interest and enthusiasm for Magento stems from one thing: Well structured system data (ahem, ladies?). Unlike other PHP systems, Magento’s architecture heavily encourages users to keep certain kinds of data well structured. For example, if a third-party programmer was hell-bent on on adding a new admin navigation menu with only HTML they probably could, but it’s much easier to take advantage of the adminhtml menu configuration system.

What developers unfamiliar with Magento see as needless complexity is what makes its ecosystem of third party extensions possible. Instead of figuring out how to parse some haphazard collection of HTML tags, CSS classes, and javascript from a variety of different module providers, extensions like the Pulse Storm Launcher can quickly and easily load Magento navigation information, no matter who added it to the system.

Gathering Navigation Links

My approach to Magento extensions usually starts from the backend. That is

How do I use Magento’s APIs to glean something interesting from its data sources

In the case of the Pulse Storm Launcher, this meant getting a representation of the navigation exposed as a javascript object.

When faced with a problem like this your first thoughts may go something like

  1. OK, where does Magento store this information?

  2. In config.xml and adminhtml.xml?

  3. OK, so I better figure out how to parse these files.

While there’s nothing wrong with this approach, a more seasoned Magento developer would keep going

  1. Wait, Magento already has code for loading the configuration tree.
  2. So I’d probably be better off looking at the global config object for navigation information. Mage::getConfig(); to the rescue!

While this second approach is even better, the really lazy experienced Magento developer keeps going

  1. Well, if Magento’s rendering a navigation object, I bet someone’s already written code to read that information out of the global configuration.

Instead of reinventing the wheel let’s take a look at how the core team itself solves the problem. We may be able to repurpose their code.

Finding the Menu Block

So step one is finding the PHP Block and template code Magento uses to load the admin navigation menu. Fortunately, we have Commerce Bug on our side. Loading an admin page and scanning through the blocks tab

reveals a long list. Using a little intuition, it seems likely that the navigation block would be one of the first blocks to render. That, combined with it’s name, makes Mage_Adminhtml_Block_Page_Menu a likely candidate. Navigating around to a few more pages backs this up, as the Mage_Adminhtml_Block_Page_Menu block is a part of every page.

Commerce Bug lets us know the block’s template is adminhtml/default/default/template/page/menu.phtml

If we take a look at the template

#File: adminhtml/default/default/template/page/menu.phtml 
<div class="nav-bar">
<!-- menu start -->
<?php echo $this->getMenuLevel($this->getMenuArray()); ?>
<!-- menu end -->

    <a id="page-help-link" href="<?php echo Mage::helper('adminhtml')->getPageHelpUrl() ?>"><?php echo $this->__('Get help for this page') ?></a>
    <script type="text/javascript">$('page-help-link').target = 'magento_page_help'</script>

</div>

we see some HTML code that looks like it’s responsible for rendering the navigation. Specifically, there’s the enclosing div

<div class="nav-bar">

If we use our browser’s web inspector to view the navigation

we see out suspicions confirmed. We’ve found the menu block!

Examining the Block File

For the most part, Magento’s phtml files contain only rudimentary looping, conditional, and “getter” template code. The menu block is no exception. In particular, it looks like this line

#File: adminhtml/default/default/template/page/menu.phtml 
<?php echo $this->getMenuLevel($this->getMenuArray()); ?>

is responsible for rendering the entire navigation. If we poke around at the source of the menu block

#File: app/code/core/Mage/Adminhtml/Block/Page/Menu.php
public function getMenuLevel($menu, $level = 0)
{
    $html = '<ul ' . (!$level ? 'id="nav"' : '') . '>' . PHP_EOL;
    foreach ($menu as $item) {
        $html .= '<li ' . (!empty($item['children']) ? 'onmouseover="Element.addClassName(this,\'over\')" '
            . 'onmouseout="Element.removeClassName(this,\'over\')"' : '') . ' class="'
            . (!$level && !empty($item['active']) ? ' active' : '') . ' '
            . (!empty($item['children']) ? ' parent' : '')
            . (!empty($level) && !empty($item['last']) ? ' last' : '')
            . ' level' . $level . '"> <a href="' . $item['url'] . '" '
            . (!empty($item['title']) ? 'title="' . $item['title'] . '"' : '') . ' '
            . (!empty($item['click']) ? 'onclick="' . $item['click'] . '"' : '') . ' class="'
            . ($level === 0 && !empty($item['active']) ? 'active' : '') . '"><span>'
            . $this->escapeHtml($item['label']) . '</span></a>' . PHP_EOL;

        if (!empty($item['children'])) {
            $html .= $this->getMenuLevel($item['children'], $level + 1);
        }
        $html .= '</li>' . PHP_EOL;
    }
    $html .= '</ul>' . PHP_EOL;

    return $html;
}    

we can see that getMenuLevel appears to be an HTML rendering function. We’re not interested in HTML though. Instead, let’s look at the getMenuArray method which populates the $menu paramater used by getMenuLevel

#File: app/code/core/Mage/Adminhtml/Block/Page/Menu.php
public function getMenuArray()
{
    return $this->_buildMenuArray();
}

//...

protected function _buildMenuArray(Varien_Simplexml_Element $parent=null, $path='', $level=0)
{
    if (is_null($parent)) {
        $parent = Mage::getSingleton('admin/config')->getAdminhtmlConfig()->getNode('menu');
    }

    $parentArr = array();

    //...
}

Ah-ha! This looks more promising. The call to getMenuArray wraps a call to the protected _buildMenuArray, and _buildMenuArray looks like it’s poking around the configuration for a node named menu.

A diligent developer would now sit down and try to understand _buildMenuArray, and while I’ll always support diligence, a practical developer will just poke at the code to see what happens.

If we temporarily edit our menu.phtml file

#File: adminhtml/default/default/template/page/menu.phtml 

<?php // you could also Mage::Log($this->getMenuArray()); ?>
<?php var_dump($this->getMenuArray()); ?>
<div class="nav-bar">
<!-- menu start -->

and then clear Magento’s block output cache, we’ll see something like the following

array (size=10)
  'dashboard' => 
    array (size=5)
      'label' => string 'Dashboard' (length=9)
      'sort_order' => int 10
      'url' => string 'http://magento1point7pointzeropoint1.dev/index.php/admin/dashboard/index/key/1fcb46d58881647c989c0dbdc88a7465/' (length=95)
      'active' => boolean false
      'level' => int 0
  'sales' => 
    array (size=7)
      'label' => string 'Sales' (length=5)
      'sort_order' => int 20
      'url' => string '#' (length=1)
      'click' => string 'return false' (length=12)
      'active' => boolean false
      'level' => int 0
      'children' => 
        array (size=9)
          'order' => 
            array (size=5)
              ...
          'invoice' => 
            array (size=5)
              ...
          'shipment' => 
            array (size=5)
              ...
          'creditmemo' => 
            array (size=5)
              ...

Bingo! The call to getMenuArray returns a PHP array that contains already parsed menu information. The best part? Since this is the same method Magento uses to build its navigation, we don’t need to spend any time worrying about ACL rules/user access levels. Magento handles all that for us and we can concentrate on our application instead of if (hasAccess()): style code.

Wrap Up

This seems like a good stopping point for today. We’ve successfully identified where Magento loads it’s navigation information, and more importantly, how. We’ve also demonstrated how the ability to reason about Magento’s code can, in the long run, save us a lot of needless reinvention.

Now that we have out menu array, next time we’ll concentrate on exposing this information to every Magento admin page.

Originally published August 26, 2012
Series Navigation<< Fast and Painless Magento Admin Navigation

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 26th August 2012