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.
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
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.
Thanks this helped a ton
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
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.
Thanks Ashley, just sent you an email!
-jamaal
Same problem here. Using magento 1.4.1.1 and “checkout_onepage_controller_success_action” never seems to fire. Done debugging until the app() function call in Mage.php, but can’t find the problem
Ashley,
You completely forgot to mention the part about the “config area.” You talked about your observer’s callback not getting called in command-line context; then you pointed out some code in the parent. But you never said anything else. Can you provide working sample snippets showing your final config.xml and observer class?
Troll much? Did you read the section of the article titled:
“Why is my Observer not firing – Config areas in Magento”
The code is in that section:
Mage::app()->loadArea();
Hello,Ashley. Thanks for your great post! I have a question here, I searched magento app folder for “checkout_submit_all_after” event, it was fired in lots of places in Magento Core code. As I know, Magento just run one time of the same event in one request. So why I can still use this event in my own module in local code?
Sorry for my pool English 🙂 Thanks in advanced.
HI Ashley,
It’s really very informative & useful article. I wanted to know more about event-observer model of Magento.
Currently I am focussing on sales order placement event. I have created one custom table in Magento that contains different sales order related columns(like increment id from sales_flat_order, item_name,sku from sales_flat_order_item etc.). I connected to mysql database through PHP. I join these table to form a single query & put this query which fetches & inserts data into custom table.
Now my ultimate aim is once customer places order, this php file must get called so that all data fetched through that query get inserted into custom table…
So how event-observer model I can use for the same…do u have any sample code for the same…
Plz guide me…
Regards,
Prat
Cool post!
I have a weird situation. I am trying to hook on to sales_order_save_after event but it won’t work. To debug this I am doing Mage::log in Mage.php to see what events are firing. Suprirsing, its on printing ‘resource_get_tablename’ and ‘core_collection_abstract_load_before’ and ‘after’ few times (24 lines altogether) and nothing else. To make it more confusing, when I save a product (by going Manage Products) its prints all big list of all the events (its suppose to). Any idea how to debug this? I want to send an email out on sales_order_save_after event.