Magento Events Explained and a few Gotchas avoided!

This post is about the Magento Event system – a full explanation of how it works and a couple of issues I had with it resolved. Hope it is a help for people wrestling with the Magento event dispatch mechanism.

My particular situation was this: when automatically fetching tracking details from our carriers via a Magento cron job, the resulting Google Checkout Magento event did not fire, so the end customer was not receiving the notification properly – even though the ’shipment’ object within Magento was correctly displaying the tracking details.

So a great deal of debuggerying and I eventually found that it was because not all config ‘areas’ are loaded on all requests. Namely the events that are loaded when accessing the admin area are not the same as those that are loaded when cron runs. So the event that fires when you update a shipment from the admin has different config areas loaded than the one that fires if you update a shipment from some PHP code run during the cron process.

Before discussing the problem I had and how it was solved, let’s dig a little deeper into events in Magento – I’ll start with a summary of the event mechanism in general.

The Magento Event Mechanism

The Magento event framework is really a great idea and I think it works well in general. It’s sort of like Wordpress hooks in a lot of ways (it’s the same basic pattern) except that the binding is done within xml (reminds me a little of Spring in Java, before Guice came along and saved me from XML-Hell!)

Basically the event firing/dispatching process is made up of X steps.

The Magento code fires (dispatches) an Event

This will look something like this in the code:

Mage::dispatchEvent('admin_session_user_login_success', array('user'=>$user));

It is important to note though that the actual number of events being fired is way higher than just the number of times you see that code within Magento. Magento fires dispatches (old habits die hard, I’ll use both interchangeably from here on out) loads of implicit events too, for example here is a snippet from the base class of all Magento Objects Mage_Core_Model_Abstract

In this snippet we see how every time a model is saved, two protected functions are called; _beforeSave and _afterSave. This is important because during these methods events are fired.

 public function save()
    {
        $this->_getResource()->beginTransaction();
        try {
            $this->_beforeSave();
            if ($this->_dataSaveAllowed) {
                $this->_getResource()->save($this);
                $this->_afterSave();
            }
            $this->_getResource()->commit();
        }
        catch (Exception $e){
            $this->_getResource()->rollBack();
            throw $e;
        }
        return $this;
    }

This snippet shows two events being fired. Notice that there is a generic event and then a dynamic one where the name of the event is built based on the object being saved. This allows you to actually listen for very explicit events like “after a shipment is saved” or “before a customer is saved” as well as general ones like “before object any saved”- the possibilities are limitless. I would like to experiment using the before load and after save to implement memcaching for Magento – anyone interested in collaborating should contact me.

The other thing to notice is that objects can be passed in to an event dispatch which then in turn get passed on to the listeners. The way to pass these is through an associative array. The keys become the names of the fields on the observer object – I’ll show you that later.

    protected function _beforeSave()
    {
        Mage::dispatchEvent('model_save_before', array('object'=>$this));
        Mage::dispatchEvent($this->_eventPrefix.'_save_before', array($this->_eventObject=>$this));
        return $this;
    }

You can see that each event is actually constructed based on the object being saved. So if we were saving a Mage_Catalog_Model_Product then the event will have:

 protected $_eventPrefix      = 'catalog_product';
 protected $_eventObject      = 'product';

So that is how we fire events, there are 100’s explicitly littered throughout the Magento core code and 1000’s more implicitly fired by actions like saving and loading objects. If you don’t believe me, try putting a Mage::log() statment in the dispatchEvent function. You can fire events yourself in your own modules too, just use the dispatchEvent() function like the Magento developers do.

Binding a function to an event

The next step is to actually configure Magento to call your function when a particular event is fired. This is handled in the config.xml file as shown in the snippet below – which in this example is listening to admin_session_user_login_success. You can place this event block inside any of the config ‘areas’ – but it is important to think carefully about which one, as I will show you shortly, if you use admin or adminhtml instead of global – then you will restrict the circumstances under which your observer is bound to the event.

        <events>
	      <admin_session_user_login_success>
	        <observers>
	          <some_descriptive_phrase_for_your_listener>
	            <type>singleton</type>
	            <class>model_base/class_name</class>
	            <method>function_name</method>
	          </some_descriptive_phrase_for_your_listener>
	        </observers>
	      </admin_session_user_login_success>
    	</events>

Implementing the observer

Right so if you get this far you just need to actually implement the function that will get called, and correctly access any data that has been passed along within the event. That’s what I’ll show you in this next snippet.

 
public function salesOrderShipmentTrackSaveAfter(Varien_Event_Observer $observer) {
 
        $track = $observer->getEvent()->getTrack();
 
        $order = $track->getShipment()->getOrder();
 
        $order->getShippingMethod();
        // ...
    }

The $observer object get’s populated with variables that can be accessed via get’s and set’s courtesy of Magento’s use of the magic __get and __set – a feature which for me, the jury is still out on. The names of the variables are the keys of the associative array passed in when the event was fired (you were paying attention to that part right?) so the getTrack() function here can be called on the event because the array passed to the event looked like array('track'=>$track).

Why is my Observer not firing – Config areas in Magento

Right so that’s my little intro to events and observers in Magento out of the way, now to my problem and how I solved it.

The problem was that my event was firing but my observer was not being called. It was being called if I saved the object through the browser, but not if I ran my test harness on the command line. Weird. So after a lot of rummaging through code I found the problem was because the area of the config that bound my observer to the event, was not being loaded when the code was called on the command line – but was when it was called from the Admin controller. So on investigating why I found this code in the base Adminhtml controller (Mage_Adminhtml_Controller_Action):

 
 public function preDispatch()
    {
        Mage::getDesign()->setArea('adminhtml')
            ->setPackageName((string)Mage::getConfig()->getNode('stores/admin/design/package/name'))
            ->setTheme((string)Mage::getConfig()->getNode('stores/admin/design/theme/default'));
 
        $this->getLayout()->setArea('adminhtml');
 
        Mage::dispatchEvent('adminhtml_controller_action_predispatch_start', array());
        parent::preDispatch();

And of course in the parent I find:

loadAreaPart('adminhtml', Mage_Core_Model_App_Area::PART_EVENTS);
Mage::app()->loadAreaPart(Mage_Core_Model_App_Area::AREA_GLOBAL,
                                                        Mage_Core_Model_App_Area::PART_EVENTS);
Mage::app()->loadAreaPart(Mage_Core_Model_App_Area::AREA_FRONTEND,
                                                        Mage_Core_Model_App_Area::PART_EVENTS);
Mage::app()->loadAreaPart(Mage_Core_Model_App_Area::AREA_ADMIN,
                                                        Mage_Core_Model_App_Area::PART_EVENTS);
//Load all parts of the config
Mage::app()->loadArea('adminhtml');
Mage::app()->loadArea(Mage_Core_Model_App_Area::AREA_FRONTEND);
// etc...

Note: I don’t know why there is no constant for adminhtml in Magento – but there isn’t at the moment.

Anyway to conclude I hope this little explanation of Magento events has been helpful to some of you, and if you have to run any commandline/cron Magento code I hope this helps you to ensure you are loading the right config file areas/parts. If anyone is interested in trying to use the events to deal with some basic memcaching of objects in a bid to speed up magento, please let me know.

You might also be interested in:

  1. New Magento SMTP features: Email logging and email sending events
  2. Sitemap Submit – Magento Extension to Submit your Google Sitemap
  3. New SMTP Pro Magento Email extension released
  4. PHP 1, Java 0: The method assertEquals(Object, Object) is ambiguous for the type
  5. Google Checkout disabled – Not available with these items


Tagged as , , , , + Categorized as Google Checkout, Magento, PHP 'ers make great lovers

6 Comments

  1. Hi,

    Is it possible to stop the parent method from running from within an event method? or to pass data to the parent event?

    Ie. I want to use checkout_cart_update_items_before to check a reserved quantity db field (changing the update quantity if not available) – is that possible or should I just override the core class instead?

    Your help would be much appreciated!
    Matt

  2. Looking at where that event is fired I’d say you can manipulate the $data variable in your observer and in doing so control what the parent method then does with the qty updating.
    Mage::dispatchEvent('checkout_cart_update_items_before', array('cart'=>$this, 'info'=>$data));

    For example, the code looks for a qty in the itemInfo, $itemInfo['qty'] in your observer you could check if the requested new qty is allowable, and if it is not, set it to the old value, which you could get the same way the cart object does; $item = $this->getQuote()->getItemById($itemId); except $this would be your observer, so you’d want to call that function on the cart parameter to the observer. Does that make sense?

    You could even set a session message in your observer like “Sorry the requested quantity of that product is not available” to be friendly to your users.

  3. Thanks this helped a ton

  4. I’m still so confused about this. I am trying to get checkout_onepage_controller_success_action to work when someone checks out I need to send some SOAP data to someone else. However it never fires!

    I’ve RTFM’ed so many times and can’t figure out what’s wrong.

    [root@VO12044 etc]# cat config.xml

    0.1.0

    MyCompany_MyModule_Helper

    <!– –>

    model
    MyCompany_MyModule_Helper_MyFunction
    setMyFuction

    in app/code/local/MyCompany/MyModule/Helper

    [root@VO12044 Helper]# cat MyFunction.php
    getEvent()->getOrder();
    foreach ($order->getAllItems() as $item) {
    $fname = ($_SERVER['DOCUMENT_ROOT'].’orders.txt’);
    $fhandle = fopen($fname,”a”);
    fwrite($fhandle,$item);
    fwrite($fhandle,”BLAH”);
    fclose($fhandle);
    //echo $order->getStatus();
    //echo $content;

    }

    //$event = $observer->getEvent();
    //$order = $event->getOrder();
    //$customer = $event->getCustomer();

    //file_put_contents($_SERVER['DOCUMENT_ROOT'].’/_db_backups/array.txt’, serialize(base64_encode($order)));
    //return;
    }
    }
    //

    I can’t figure out what im doing wrong. HALP!

    jamaal@cellyeah.com

  5. The comment had all your formatting stripped – flick me an email and I’ll run an eye over that config.xml – the php code you pasted in doesn’t look quite right either, but that may be related to the comment formatting.

  6. Thanks Ashley, just sent you an email!

    -jamaal

Leave a Comment

Name:

Email:

Website:

Sporadic Tweeting...

What I'm listening to

  • The Black Keys - Brothers
  • Mos Def - Black On Both Sides
  • Foo Fighters - Skin and Bones
  • The Black Keys - Chulahoma
  • The White Stripes - Icky Thump
  • The Naked and Famous - This Machine
  • The Black Keys - The Moan
  • Red Hot Chili Peppers - Blood Sugar Sex Magik
  • The Naked and Famous - No Light