Magento Tutorials

Growl-like notifications in the Magento backend

Growler notifications in the Magento backend

Earlier this week, we glossed over Magento its messages system. Today, we will be customizing those messages in the backend by replacing them with Growl-like notifications.

Growler

Those working with Mac OS, might already know Growl. Due to its clean and unobtrusive notifications, it has spawned a lot of Growl-like notification interface elements and Javascript plugins for the web. A popular version is jQuery Growl.

Magento its backend uses Prototype (and script.aculo.us) instead of jQuery. Throwing in jQuery is far beyond the purpose of this tutorial, so we will use what is available to us and settle with the bit outdated but working Growler for Prototype

Identifying the problem

To get those fancy Growl notifications in the Magento backend, there are only two steps to take. Fair warning, step two isn’t without a few hiccups.

  1. Include the Growler plugin – a JavaScript file and some CSS
  2. Customize the existing messages their output so they are rendered with Growler

Easy things first

The first step is pretty easy so we will get that over with real quick. First, we create the stub of a Magento module: an etc/config.xml file and an empty Model directory. Ours will live in the (local) Magenticians_GrowlerMessages namespace:

<!-- no magic here, just the absolute necessary to get started -->
<?xml version="1.0"?>
<config>
    <modules>
        <Magenticians_GrowlerMessages>
            <version>0.0.1</version>
        </Magenticians_GrowlerMessages>
    </modules>

    <global>
        <models>
            <growlermessages>
                    <class>Magenticians_GrowlerMessages_Model</class>
            </growlermessages>
        </models>
    </global>
</config>

Because we want to update the backend layout, aliased as adminhtml, we will tell Magento the module has a layout-file with updates to be processed. growlermessages.xml contains layout definitions for the default adminhtml skin, so it finds its place in /app/design/adminhtml/default/default/layout.

    <!-- ... -->
    </global>
    
    <adminhtml>
        <layout>
            <updates>
                <growlermessages>
                    <file>growlermessages.xml</file>
                </growlermessages>
            </updates>
        </layout>
    </adminhtml>
</config>

Its contents are very straight forward: telling the layout processor we want to include Growler its footprint on every page:

<?xml version="1.0"?>
<layout>
    <default>
        <reference name="head">
            <action method="addItem"><type>js</type><name>growlermessages/growler.js</name></action>
            <action method="addItem"><type>skin_css</type><name>growlermessages/css/growler.css</name></action>
        </reference>
    </default>
</layout>

Last but not least, we add a template-file to the layout definition which can be used to render our custom JavaScript for initiating Growler. We assign it to the js block, an adminhtml section designated for JavaScript to be included, located near the end of the document. This template-file is retrieved relative from /app/design/adminhtml/default/default/template. Because we do not need a custom block, we will borrow the default adminhtml/template behavior.

        <!-- ... -->   
        </reference>

        <reference name="js">
            <block type="adminhtml/template" as="growlermessages_js" name="growlermessages_js" template="growlermessages/js/growler.phtml" />
        </reference>
    </default>
</layout>

As soon as you put a standard definition of the just created module in /app/etc/modules and look at the source code of any backend page, you should see the Growler-footprint in the section. Because the js/growler.phtml file is probably still empty, put some “#testing” string in it and you should be able to find it in the source, too.

Awry with getGroupedHtml

Due to the nature of Magento its messages (tightly coupled and HTML in core files) the problem is that there is no direct way to achieve what we want. This is because the adminhtml_block_html_before event often used for customizing backend blocks, is fired in the _toHtml method of Mage_Adminhtml_Block_Template, but the class used to render the messages block (Mage_Adminhtml_Block_Messages) does not inherit from the former at all.

Is there any other event which comes close to the messages block right before rendering? Not at this moment. The messages get most often rendered directly with getGroupedHtml() in the adminhtml its page.phtml template. If you really wanted, you could inherit the entire template and replace the few bad lines with custom rendering of the messages but it is not a consistent approach seeing how it is not the only place where getGroupedHtml is called.

So you can either fix Magento’s dirty quirks by manually fixing all the bad adminhtml templates, or get creative and use a bit more drastic approach.

Bypassing the Magento core

Looking at getGroupedHtml of Mage_Core_Block_Messages which Mage_Adminhtml_Block_Messages inherits from, we see that there are no events being dispatched whatsoever. Just a simple foreach loop generating HTML in a core file. What’s all the hassle about anyway?!

Anyhow, they key in customizing the output of the Magento messages, is that we do not need the old behavior at all. Bypassing it, is as simple as pretending there are no messages to be output at all. For this, we will have to clear the messages storage in a controller_action_layout_generate_blocks_before observer. This event is dispatched right before any block gets the chance to include them by calling getGroupedHtml.

Updating config.xml its adminhtml section with a proper observer is the next task:

<events>
    <controller_action_layout_generate_blocks_before>
        <observers>
            <magenticians_growlermessages_relaymessages>
                <class>growlermessages/Observer</class>
                <method>relayMessages</method>
            </magenticians_growlermessages_relaymessages>
        </observers>
    </controller_action_layout_generate_blocks_before>
</events>

We are naming our observer its method relayMessages because it moves the original messages storage to a different place, namely the messages_collection key in the Magento registry:

class Magenticians_GrowlerMessages_Model_Observer
{
    public function relayMessages(Varien_Event_Observer $observer)
    {
        // Retrieve the messages collection from adminhtml session and put it in the registry
        // true parameter passed to getMessages will make sure they get cleared from the session
        Mage::register('messages_collection', Mage::getSingleton('adminhtml/session')->getMessages(true));
    }
}

Navigating through the backend and performing some actions which used to spawn messages, like saving store configuration, should now be very silent.

The template file

Now that we have the messages in the registry, we can access them in the js/growler.phtml template-file we created earlier. We simply iterate over them, peek at the Growler examples and include the correct function calls. An example:

g.info('Growl, growl, here is some info!', {classname: 'plain'});

Before we get to these calls, we start our growler.phtml file with checking whether there are any messages. Seeing as the template is included on every page load in the backend, we don’t want to be held accountable for slowing down page load, heh.

<?php
$_messages = Mage::registry('messages_collection');

if (is_null($_messages)) {
    return;
}

$_messages = $_messages->getItems();

if (empty($_messages)) {
    return;
}
?>

$_messages is now an array with instances of Mage_Core_Model_Message_Abstract, Mage_Core_Model_Message_Success for example. Because all of its fields are protected, json_encode will not function properly. We have to build a proper array ourselves.

$_messagesData = array();

foreach ($_messages as $_message) {
    /** @var $_message Mage_Core_Model_Message_Abstract */
    $_messagesData[] = array(
        'type' => $_message->getType(),
        'text' => $_message->getText()
    );
}
?>

<script type="text/javascript">
// To be continued...
</script>

Note that messages have a few other data points, but type and text are all we need. isSticky is such a field, but we haven’t found any occurrences in the Magento code base of messages having that field set to something other than the default value.

On to the JavaScript

Now that we have a clean $_messagesData array, we can simply encode it as JSON so it can be accessed from JavaScript:

<script type="text/javascript">
    var messages = <?php echo json_encode($_messages); ?>;
</script>

Because the Growler message types (error, warn, growl and info) do not directly translate to Magento its message types (error, warning, notice and success), we will have to use a lookup-table:

// Magento -> Growler
var typeTranslation = {
    error: 'error',
    warning: 'warn',
    notice: 'growl',
    success: 'info'
};

The last piece of the puzzle is iterating over messages, calling the appropriate method as defined in typeTranslation and supply the message its text. Each growl will have a classname of plain for the appropriate CSS styling to get attached.

var growler = new Growler({});

for (var i = 0; i < messages.length; i++) {
    var message = messages[i];
    growler[typeTranslation[message.type]](message.text, {classname: 'plain'});
}

Issues with AJAX

You should now be able to save your store configuration and see a Growler notification pop-up. Looks nifty, right? Odd thing is, that saving a category entity will not reveal any messages whatsoever! This is because that is one of the actions which happens with the help of AJAX requests.

Because we are clearing all messages just before rendering, these are not passed over. It can quickly be fixed by checking in our observer whether we are dealing with an AJAX request:

public function relayMessages(Varien_Event_Observer $observer)
{
    if (Mage::app()->getRequest()->isXmlHttpRequest()) {
        return;
    }

    Mage::register('messages_collection', Mage::getSingleton('adminhtml/session')->getMessages(true));
}

AJAX requests which spawn messages, should now start appearing as original again. We could leave it like this, or start using some magic to even get messages pulled in via AJAX, working with Growler.

JSON is the key

Searching through the adminhtml codebase for getGroupedHtml will reveal that some controllers put the result of that call in the ‘messages’ or ‘message’ key in a JSON encoded response. Backend JavaScript will check if such a key is set, and put the contents in the #messages DOM-node.

Though Magento utilizes a few JavaScript events, there is not something we can use. Even if we were able to somehow intercept the response object and alter the key holding the messages client-side, we are dealing with raw HTML we have to iterate over.

A better approach is to alter the response object on the server so the ‘messages’ and ‘message’ key will have a proper JSON response instead of raw HTML. On the client, we can then use Prototype its global AJAX events to ‘catch’ and process the JSON.

Manipulating the response

Right before Magento tidies up and returns the response to the browser, the http_response_send_before event is fired for last-minute changes to be applied. Let’s set up an observer:

<events>
    <http_response_send_before>
        <observers>
            <magenticians_growlermessages_altermessagesresponse>
                <class>growlermessages/Observer</class>
                <method>alterMessageResponse</method>
            </magenticians_growlermessages_altermessagesresponse>
        </observers>
    </http_response_send_before>
</events>

In the observer, non-AJAX requests are ignored. All other requests their responses will have a body-check for valid JSON. If this is the case, we will check the body its contents for either a ‘message’ or ‘messages’ key. The contents will be replaced with a JSON encoded array of messages which can be easily fed to Growler:

public function alterMessageResponse(Varien_Event_Observer $observer) {
    if (! Mage::app()->getRequest()->isXmlHttpRequest()) {
        // Ignore non-AJAX requests
        return;
    }

    $responseBody = $observer->getEvent()->getResponse()->getBody();

    // Attempt to decode the response object, non-JSON responses will be skipped
    try {
        $responseBodyDecoded = Mage::helper('core')->jsonDecode($responseBody);
    } catch (Zend_Json_Exception $e) {
        return;
    }

    // Intercept our target keys and replace with our own JSON encoded array
    foreach (array('message', 'messages') as $target) {
        if (isset($responseBodyDecoded[$target])) {
            $responseBodyDecoded[$target] = json_encode(Mage::helper('growlermessages')->getMessages());
        }
    }

    // Ship our modified body to the response
    $observer->getEvent()->getResponse()->setBody(Mage::helper('core')->jsonEncode($responseBodyDecoded));
}

Note that the Mage::helper(‘growlermessages’)->getMessages() call is nothing more than the code we earlier wrote directly in js/growler.phtml, wrapped in a helper class.

PrototypeJS AJAX events

Without further change, performing an AJAX request in the Magento backend, like saving a category entity, will show the messages in JSON-format where the messages used to spawn. This is good, because now we only have to evaluate the JSON-blob and feed it to Growler!

In our js/growler.phtml, we will set up a global AJAX Responder on the onComplete event. This event is dispatched at the very end of a (Prototype) AJAX request its life-cycle. Therefore, we know the backend JavaScript already added the JSON-blob to the #messages container:

Ajax.Responders.register({
    onComplete: function() {
        // Always hide the messages container because it might contain a JSON-blob
        $('messages').hide();
        
        if (! $('messages').innerText.isJSON()) {
            return;
        }

        var messages = $('messages').innerText.evalJSON();
        
        for (var i = 0; i < messages.length; i++) {
            var message = messages[i];
            growler[typeTranslation[message.type]](message.text, {classname: 'plain'});
        }
    }
});

Above, we always hide the #messages container as it might contain a JSON-blob. Then, we check whether the container its contents is valid JSON and if so, evaluate it and save the result in the messages variable. The for-loop we use, should look familiar as we used it earlier.

And finally, doing anything over AJAX which spawns messages, should now also show Growler messages.

Conclusion

You might have to get creative and look beyond common tricks, but in the end, it’s unlikely Magento imposes limitations due to badly written legacy code or simply because it is too coupled with the core to alter properly. Sure, in an ideal situation all of this should have been a lot easier, but the key to take away is that there is not much which isn’t possible with Magento after some research and work.

All of the code used in this tutorial is available on Github for you to skim over or use as reference material. Though the resulting module might get you some fancy Growler messages in the backend, it was never the intention of this article to “pimp your backend”. Rather, to show you how to achieve customization with the resources Magento developers have at their disposal.

Subscribe Newsletter

Subscribe to get latest Magento news

40% Off for 4 Months on Magento Hosting + 30 Free Migration