Magento Under-the-Hood: Events and Observers

The observer pattern is an ideal form of extending a program’s functionality. In a nutshell, it allows you to run arbitrary code by subscribing to events that are triggered during program execution. Since it is part of Magento’s architecture, this allows you to inject your own routines into the e-commerce process.

Using observers rather than class overrides is considered best practice. For instance, overriding a Magento class involves duplicating the entire original file (in most cases to only to change a single line). Furthermore, if the next version of Magento contains changes to the class you overrode, then you must merge the changes manually to ensure your override doesn’t break your store. This is just too much overhead for my liking. On the contrary, observers often involve fewer lines of code and are thus easier to maintain.

So the moral of the story is: use an observer whenever you can.

Under-the-Hood: How Are Events Fired?

At the root of it all, the Mage::dispatchEvent() function triggers the occurrence of an event. There are calls to the dispatch function throughout the entire Magento core, and if you’re writing your own extensions then you should plan to have your own events that are dispatched. This is simply good practice. The dispatching function is located in app/Mage.php. Despite being a wrapper around the application model, it still gives us a good idea of what is going on.

app/Mage.php

    /**
     * Dispatch event
     *
     * Calls all observer callbacks registered for this event
     * and multiobservers matching event name pattern
     *
     * @param string $name
     * @param array $args
     * @return Mage_Core_Model_App
     */
    public static function dispatchEvent($name, array $data = array())
    {
        Varien_Profiler::start('DISPATCH EVENT:'.$name);
        $result = self::app()->dispatchEvent($name, $data);
        Varien_Profiler::stop('DISPATCH EVENT:'.$name);
        return $result;
    }

We can see that the function takes two arguments: a name for the event, and an array of data. Searching for all of the occurrences of the string Mage::dispatchEvent in your Magento folder will give you an idea of how many events get dispatched by Magento. Or you can refer to a list of all Magento events here. Since this function is merely a wrapper that calls on the application model, let’s treat it like a black box for now.

Creating Observers

Observers are declared in Magento modules, and cannot be declared in a theme. I am assuming you are already familiar with the structure of a module (otherwise familiarize yourself with module development first). Here are the basic steps to create an observer:

  1. Add an <events /> section in [YourNamespace]/[Module]/etc/config.xml
  2. Create a new file [YourNamespace]/[Module]/Model/Observer.php that will map to the Magento class path “yourmodule/observer” (this class does not need to extend from any core models).
  3. Create a function that takes a single argument: $observer. This function will run our custom code.

app/code/local/YourNamespace/Module/etc/config.xml

<config>
    <modules>
        <YourNamespace_Module>
            <version>0.0.0</version>
        </YourNamespace_Module>
    </modules>
    <global>
        <models>
            <yourmodule>
                <class>YourNamespace_Module_Model</class>
            </yourmodule>
        </models>
        <events>
            <sales_order_place_after>
                <observers>
                    <yournamespace_module_sales_order_place_after>
                        <class>yourmodule/observer</class>
                        <method>doSomethingSpecial</method>
                    </yournamespace_module_sales_order_place_after>
                </observers>
            </sales_order_place_after>
        </events>
    </global>
</config>

app/code/local/YourNamespace/Module/Model/Observer.php

<?php
/**
 * Our observer model for handling any dispatched Magento events. Does not
 * need to inherit from any core classes, although extending Varien_Object
 * may come in useful.
 */
class YourNamespace_Module_Model_Observer extends Varien_Object {
    /**
     * This function is triggered by a Magento observer declared
     * in etc/config.xml
     *
     * @param Varien_Event_Observer $observer
     */
    public function doSomethingSpecial($observer)
    {
        // Your magic code goes here...
    }
}

The preceding examples create an observer that will subscribe to the sales_order_place_after event. This is a common event to process if you want to run additional code whenever an order is placed, such as send additional email alerts or pass order information to some third-party API.

What Gets Passed to the Observer Function?

So what exactly is the $observer that is being passed to our function? Earlier we took a cursory look at Mage::dispatchEvent(). Let us now take a look at the application model’s implementation of the dispatcher to get a deeper understanding of the data passed to the observer.

app/code/core/Mage/Core/Model/App.php

    public function dispatchEvent($eventName, $args)
    {
        foreach ($this->_events as $area=>$events) {
            if (!isset($events[$eventName])) {
                $eventConfig = $this->getConfig()->getEventConfig($area, $eventName);
                if (!$eventConfig) {
                    $this->_events[$area][$eventName] = false;
                    continue;
                }
                $observers = array();
                foreach ($eventConfig->observers->children() as $obsName=>$obsConfig) {
                    $observers[$obsName] = array(
                        'type'  => (string)$obsConfig->type,
                        'model' => $obsConfig->class ? (string)$obsConfig->class : $obsConfig->getClassName(),
                        'method'=> (string)$obsConfig->method,
                        'args'  => (array)$obsConfig->args,
                    );
                }
                $events[$eventName]['observers'] = $observers;
                $this->_events[$area][$eventName]['observers'] = $observers;
            }
            if (false===$events[$eventName]) {
                continue;
            } else {
                $event = new Varien_Event($args);
                $event->setName($eventName);
                $observer = new Varien_Event_Observer();
            }

            foreach ($events[$eventName]['observers'] as $obsName=>$obs) {
                $observer->setData(array('event'=>$event));
                Varien_Profiler::start('OBSERVER: '.$obsName);
                switch ($obs['type']) {
                    case 'disabled':
                        break;
                    case 'object':
                    case 'model':
                        $method = $obs['method'];
                        $observer->addData($args);
                        $object = Mage::getModel($obs['model']);
                        $this->_callObserverMethod($object, $method, $observer);
                        break;
                    default:
                        $method = $obs['method'];
                        $observer->addData($args);
                        $object = Mage::getSingleton($obs['model']);
                        $this->_callObserverMethod($object, $method, $observer);
                        break;
                }
                Varien_Profiler::stop('OBSERVER: '.$obsName);
            }
        }
        return $this;
    }

As the parameters are passed to the App model, we see that the name argument matches against what is declared in the config.xml and the array of data gets set on the $observer so that we can access the data in our code.

The observer function will receive an object of type Varien_Event_Observer. This is simply a Varien_Object with some essential data points that are specific to handling events. Most commonly, you would do something like $observer->getEvent()->getOrder() if you are observing an event in the sales/order model; or if you are doing something more generic, like observing a model_save_after event, you would do something like $observer->getEvent()->getObject().

How Do I Create an Admin-Only Event?

If you don’t want your event to be triggered by a frontend order, but you want it to execute when an order is placed in the backend, simply declare your event-observer in the <admin> section of the config:

app/code/local/YourNamespace/Module/etc/config.xml

<config>
    <modules>
        <YourNamespace_Module>
            <version>0.0.0</version>
        </YourNamespace_Module>
    </modules>
    <global>
        <models>
            <yourmodule>
                <class>YourNamespace_Module_Model</class>
            </yourmodule>
        </models>
    </global>
    <admin>
        <events>
            <sales_order_place_after>
                <observers>
                    <yournamespace_module_sales_order_place_after>
                        <class>yourmodule/observer</class>
                        <method>doSomethingSpecial</method>
                    </yournamespace_module_sales_order_place_after>
                </observers>
            </sales_order_place_after>
        </events>
    </admin>
</config>

Tip: Accessing URL Parameters (the Magento Way)

Sometimes you want your observer to do something based on a URL parameter, but the $observer only has access to a finite list of objects that doesn’t include GET and POST variables. We can do this using Mage::app()->getRequest()->getParams(). That will give us the entire parameters array without needing to use $_GET or $_POST directly. If you want to retrieve a single parameter, simply pass a string to the getParams() part.

You can also use Mage::registry('some_registry_key') to retrieve any values that have been registered by core Magento controllers.

Conclusion

Magento observers are very powerful and are an excellent tactic to master. Hopefully some of the tips and explanations that I have outlined above will give you a better understanding of how to bend Magento to your will!