Solving Magento

Solutions for Magento E-Commerce Platform

by Oleg Ishenko

Event-Driven Architecture in Magento: Observer Pattern

Imagine a situation: you are developing a custom e-commerce framework and are about to put some finishing touches to your order model. The task is to program functionality, which creates an order at the end of the checkout and saves it to the database. You realize that a confirmation email has to be sent and you add the necessary code. Next morning a product manager asks you to send a copy of the e-mail to his address, and you extend your code accordingly. At a meeting in the afternoon the warehouse guy proposes to automate inventory management by sending a message to the warehouse software; and you implement it in your code. Exporting order as an XML file to feed into the shipping system – done. Notifying the purchasing department when the inventory is too low – done too.

After all this you take a look at your order model and its function placeOrder and see it has become an unmaintainable mess. All these diverse and complex tasks just do not fit into the order model. Yet they are essential for the your business and must be implemented. Situations like this are not uncommon. The growing complexity of enterprise applications often results in code that is inflexible and difficult to maintain, and prohibitively expensive to scale.

Event-driven software architecture has evolved to address such problems by decoupling services and service providers. It introduces events – notable milestones in business processes that invoke services, which observe and react to them. Events alert their subscribers about a problem, or an opportunity, or a threshold in the current process flow. An event broadcasted in the system usually consists of an event header and body. The event header contains an ID that is used to locate subscribers, while the body transports information required to process the event. In some systems event headers can also include information on the event type and creator, or a timestamp – whatever data the specifications mandates.

The service providers are independent entities and can be added or removed without affecting objects, whose events they listen to. Event creators have no knowledge of the subscribed service providers and do not depend on them. Similarly service providers are not interested in the internal mechanics of event creators. This allows for extremely flexible, loosely coupled and distributed systems. This advantages, however, come at a price – tracing events and their subscribers can be difficult.

Events and Observers in Magento

Events in Magento are triggers that tell the system to look for subscribers, which must be executed when an event is fired. To fire an event you need to specify an event code – a string, e.g. my_custom_event_code. The event code should be descriptive. If the event is distinct, its code must be unique in the system. Optionally you can pass some data along with the event. We will talk about passing data with the event in detail a bit later. An example of an event fired in Magento:

Mage::dispatchEvent('event_code', array('myObject' => $myObject));


Event subscribers in Magento are class functions, which are defined in the <observers> node of the system configuration. Every time an event is fired, the system tries to locate these subscribers and execute their code. By convention, subscribers are implemented in classes, whose names contain the word observer. As an example let’s consider a core module Mage_SalesRule. In its Model folder you can find a file called Observer.php containing a class Mage_SalesRule_Model_Observer. An observer class does not have to extend any other class in a way the models usually extend Mage_Core_Model_Abstract. However, if the situation dictates, you are free to implement an observer as an extension of some other class. The function Mage_SalesRule_Model_Observer::sales_order_afterPlace is an example of an event subscriber. It is registered to observe event sales_order_place_after, which is fired right after an order is placed – in function Mage_Sales_Model_Order::place().

Observer Configuration

Observers are registered in module configuration files, config.xml. A typical structure of an observer configuration:

<global> <!-- this node can be <frontend> or <adminhtml> -->
        <events>
                <event_code> <!-- replace event_name with an event identifier, e.g. sales_order_place_after -->
                        <observers>
                                <your_observer_node_name> <!-- a custom identifier for your observer configuration -->
                                        <class>yourpackage_yourmodule/observer</class> <!-- a pointer to your observer model -->
                                        <method>yourObserverFunctionName</method> <!-- name of the function in your observer class -->
                                        <type>singleton</type> <!-- optional node defining how the observer class is instantiated -->
                                </your_observer_node_name>
                        </observers>
                </event_code>
        </events>
</global>

More details on the configuration nodes:

The system reads the observer configuration from <events> nodes. When editing your module’s configuration file, you can place this node (and with it your observer registration) into either <global>, <frontend>, or <adminhtml> nodes. To decide, which of these three nodes to use as a parent, think if the business logic of your module places any restrictions on the execution of your observer function. Some observers must be processed only when the event is fired in the back-end, i.e. to respond to an action by a shop administrator or a shop API call. Other events have to be processed only if they are fired by a customer interacting with the shop front-end. If no such restriction is imposed – you are free to put your configuration into the <global> node.

Restricting Observers to Frontend or Adminhtml Events

If, however, you need to restrict the reaction of your observers to events fired within the back-end or the front-end part of your shop – then place their configuration into the <frontend> or the <backend> node respectively. It is important to realize that any event can be fired from anywhere in the application. But you can choose to configure your observer to react to events from one of the two shop areas. Or, again, react to events no matter where they have been fired from, i.e. globally.

The <event_name> node contains the code of the event your observer must react to. For example sales_order_place_after, or customer_login. Finding out a right event can be tricky, we will look into a few possible ways later.

Use the <your_observer_node_name> node to specify an identifier for your configuration. It is better to make sure it is unique so that it wouldn’t conflict with observer configurations set in other modules. A good candidate for such identifier is a combination of your package and module names: <mypackage_mymodule_observer>

The system uses nodes <class>, <method>, and <type> to locate your event subscriber.

The node <class> must contain a reference to an observer class, which contains the subscriber function. This reference can be in the common Magento model identifier form mymodule/mymodel, e.g. salesrule/observer. Alternatively it can be just a plain class name like MyPackage_MyModule_Model_Observer or (staying with the SalesRule example) Mage_SalesRule_Model_Observer.

The node <method> is simply a name of the function your observer class contains and which must be called in a response to the event. The SalesRule module’s observer function in our example is sales_order_afterPlace.

The <type> node is optional. Unless it is specified, Magento will instantiate your observer as a singleton. This means that whatever properties are assigned to your observer will be available to the system as long as the request processing is not over. For example, if you define a constructor function for your observer which sets a current millisecond timestamp to some internal property, the value set to this property at the first call will be the same as at any subsequent calls. This is because once the singleton is instantiated it stays there and no new instances of your observer are created. If you think your code will have problems because of that, set this node to model:

<type>model</type>

This will ensure that an new instance of your observer class is created every time the event it is subscribed to is fired.

Finally, a completed configuration example from the config.xml file of the module Mage_SalesRule:

<global>
    <!-- some nodes omitted -->
    <events>
        <sales_order_place_after>
            <observers>
                <salesrule>
                    <class>salesrule/observer</class>
                    <method>sales_order_afterPlace</method>
                </salesrule>
            </observers>
        </sales_order_place_after>
        <sales_quote_config_get_product_attributes>
            <observers>
                <salesrule>
                    <class>salesrule/observer</class>
                    <method>addProductAttributes</method>
                </salesrule>
            </observers>
        </sales_quote_config_get_product_attributes>
        <sales_convert_quote_to_order>
            <observers>
                <salesrule>
                    <class>salesrule/observer</class>
                    <method>addSalesRuleNameToOrder</method>
                </salesrule>
            </observers>
        </sales_convert_quote_to_order>
    </events>
        <!-- some nodes omitted -->
</global>

Observer Functions and Passing Event Objects to Observers

Observer functions take one parameter – $observer of type Varien_Event_Observer. Its primary role is to pass data from the code that fired the event to the observer function. For this purpose the class Varien_Event_Observer has a data property event. This property is an object of class Varien_Event and it contains the data, which was passed along with the fired event. Staying with our example, after an order is placed the system fires a sales_order_place_after event:

Mage::dispatchEvent('sales_order_place_after', array('order' => $this));

Here $this is the order object itself. It is passed on to the observer function in an associative array. Its key is ‘order’ – and under this identifier the order object will be available to the observer function. You can pass several objects or variables to the observer function by adding more elements to that array, e.g.:

Mage::dispatchEvent(
    'sales_order_place_after',
    array('order' => $this, 'checkout' => Mage::getSingleton('checkout/session')
);

This will pass the order object and a checkout session singleton to the observer function.

The observer function uses its parameter $observer to extract the data passed for processing:

public function sales_order_afterPlace($observer)
{
        $order = $observer->getEvent()->getOrder();
        /** code omitted for brevity **/
}

As you can see, $observer returns an object of type Varien_Event that it has in its data storage like any other object inherited from the class Varien_Object. The Varien_Event in turn, returns the order object passed by the code that fired the event. The order is also stored in the data and is retrieved by the magic getter function.

Dispatch Event Flow

The event processing starts with some code in Magento calling Mage::dispatchEvent($eventName, $args);. In this call $eventName is a string containing a unique event identifier. The parameter $args is an associative array. Its keys are names by which the objects (elements of the array) can be referred to in the function that catches and processes the event. For example, the model Mage_Sales_Model_Order dispatches events sales_order_place_before and sales_order_place_after from its function place():

public function place()
{
        Mage::dispatchEvent('sales_order_place_before', array('order'=>$this));
        $this->_placePayment();
        Mage::dispatchEvent('sales_order_place_after', array('order'=>$this));
        return $this;
}

The second parameter in these calls contains the order object itself. This way any function, which is subscribed to these events, can access the properties of the object that has triggered them.

The function Mage::dispatchEvent doesn’t do much itself. It delegates the event broadcasting to a function under the same name in the Mage_Core_Model_App class. The function Mage_Core_model_App::dispatchEvent loops through the internal event configuration storage of the app singleton. Depending on where in the application the event was fired, this configuration can include global observers only, or global and either frontend or adminhtml observers. If a match is found the system creates an instance of the observer class (it can be a regular model object or a singleton depending on the observer configuration) and calls the subscriber method. It also passes an Varien_Event_Observer method to the subscriber. If the event configuration contains multiple observers they all will be processed in this manner in a loop. Once all the subscribers are executed the function returns control back to the code that fired the event.

Events Available in Magento

Magento offers a number of standard events covering processes of loading, saving, and deleting of models. The names of these events end with:

  • _load_before
  • _load_after
  • _save_before
  • _save_after
  • _save_commit_after (thrown after a save transaction is committed, after the _save_after event)
  • _delete_before
  • _delete_after
  • _delete_commit_after (thrown after a delete transaction is committed, after the _delete_after event)

Classes extending the abstract model Mage_Core_Model_Abstract have a protected property _eventPrefix which is used to generate event names by combining it with the event name stubs from the list above:

Mage::dispatchEvent($this->_eventPrefix.'_load_after', $this->_getEventData());

For example, the Mage_Sales_Model_Order class’s event prefix is sales_order, so the actual “after loading” event’s name will be sales_order_load_after. Other event names are generated in this manner: catalog_category_load_before, catalog_product_delete_after, customer_save_before, etc.

Not only models but also collections throw events on loading and saving. Collection classes inherit the events functionality from their abstract class Mage_Core_Model_Resource_Db_Collection_Abstract. The event triggering can be found in the protected functions _beforeLoad and _beforeSave, e.g.:

protected function _beforeLoad()
{
    parent::_beforeLoad();
    Mage::dispatchEvent('core_collection_abstract_load_before', array('collection' => $this));
    if ($this->_eventPrefix && $this->_eventObject) {
        Mage::dispatchEvent($this-&gt;_eventPrefix.'_load_before', array(
            $this->_eventObject => $this
        ));
    }
    return $this;
}

As you can see, collections also use the _eventPrefix property to generate event names in the children classes.

Besides the standard model events Magento makes use of nearly 400 (!) specific events. You can locate them by searching the codebase for the string Mage::dispatchEvent(. To restrict the number of results, search not the entire code but only the modules you are interested in.

The following Linux command will do the search for you and put the results into a file:

grep -r "Mage::dispatchEvent(" app/code > events.txt

Or you can use this event cheat sheet compiled by Nick Jones

When developing Magento extensions you can define your own events and register observers that catch them. We will look into a few examples in the next post.

Readers who read this post also read these:

  • Events and Observers: a Magento Tutorial
    This tutorial will demonstrate the principles of the observer pattern implemented in Magento. The details of this implementation have already been discussed in the previous part Event-Driven Archit...
  • Magento OnePage Checkout Part 1
    It is hard to overstate the importance of the checkout. You can’t have a web shop without a checkout - it is where customers exchange money for goods and services, which is, basically, the whole pu...
  • Magento Downloadable Product Type (Part 1)
    Magento’s Downloadable product type is meant to sell files. Software, e-books, images, music or video - any type of content that can be packaged into files and downloaded can be sold using this pro...
  • Magento Bundle Product Type (Part 2)
    This is the second part of our discussion on Magento Bundle product type. In the first part we concentrated on the back-end management of bundle products and on the front-end display specifics. In ...

18 thoughts on “Event-Driven Architecture in Magento: Observer Pattern

  1. Pingback: Events and Observers: a Magento Tutorial | Solving Magento

  2. Fantastic tutorial.

    I came across this while looking at the logic behind $observer->getEvent()->getOrder() as I can’t locate where the getOrder() function is.

    Am I right in thinking (having dug through the getEvent() method) that getOrder() returns the element inside the array with ‘order’ as key; so if I wanted the checkout session in your example, I’d use $observer->getEvent()->getCheckout()?

    Many thanks in advance for any help :)

    • Hello Stan,

      Your assumption is correct. The event object’s property _data is an array, whose keys are used to access the data passed by the code that dispatches the event. Thus $event->getOrder() is equal to $event->getData('order') and will return the $event->_data[‘order’] value. In my example the event does contain a checkout object, which can be accessed that way.

      Thank you for your comment, and if you have further questions please post them here.

      • Thank you Oleg!

        That’s a great explanation! I think we (my coder friend and I) are finally getting the hang of this! 😀

        Keep up the good work!

  3. Hello Oleg,

    I am Magento developer and I am on magento learning face. This Post is really really good for people like me.

    Thanks.

  4. Hello Oleg,

    I am new to mageno. I believe that this is best explanation to understand the magento EDA {event driven architecture }.

    Thanks for the nice post…!

  5. Great article….

    I was struggling to modify a community plugin and was not able to trace the path as it was using Observer pattern.

    This article and the comments helped me a lot.

    Keep up the good work

    Thanks

Leave a Reply

Your email address will not be published. Required fields are marked *

Theme: Esquire by Matthew Buchanan.