Solving Magento

Solutions for Magento E-Commerce Platform

by Oleg Ishenko

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 Architecture in Magento: Observer Pattern. Now we will apply this knowledge in practice and develop a module, whose functionality is built upon events and observers.

The module will perform the following tasks:

  1. An XML export file is created every time an order is placed in the shop.
  2. Every time a new customer places his first order an email is dispatched to the shop manager.

As usual, the first step to create a module is adding a module configuration file to the app/etc/modules directory. The local code pool’s package of the module is Solvingmagento and the module name is OrderExport. Place the following configuration into the app/etc/modules/Solvingmagento_OrderExport.xml:

<?xml version="1.0"?>
	<config>
	    <modules>
		<Solvingmagento_OrderExport>
		    <active>true</active>
		    <codePool>local</codePool>
		</Solvingmagento_OrderExport>
	    </modules>
	</config>


Once this is done, we create a folder structure for the new module:

	app/
		code/
			local/
				Solvingmagento/
					OrderExport/
						Helper/
						Model/
						etc/
						sql/
							solvingmagento_orderexport_setup/

We also add a basic module configuration to the etc/config.xml:

<?xml version="1.0" encoding="UTF-8"?>
	<config>
	    <modules>
		<Solvingmagento_OrderExport>
		    <version>0.1.0</version>
		</Solvingmagento_OrderExport>
	    </modules>
	    <global>
		<helpers>
		    <orderexport>
			<class>Solvingmagento_OrderExport_Helper</class>
		    </orderexport>
		</helpers>
	    </global>
	</config>

The helper configuration is added to enable the internationalization functionality for the module. Go ahead and create a Data.php file in the Helper folder with the following content:

	class Solvingmagento_OrderExport_Helper_Data extends Mage_Core_Helper_Abstract
	{
	}

As you can see, the helper class is empty – for this module we don’t need any extra functionality apart from that what is inherited from the Mage_Core_Helper_Abstract class.

This concludes the boilerplate preparation of the module and now we can start implementing the actual required functionality.

In order to create an XML export file for every order at the moment an order is placed we will observe the core event called sales_order_place_after. This event is dispatched after the order payment is placed from the function Mage_Sales_Model_Order::place(). We will create a model-observer whose function will react to this event. First things first: add the necessary model configuration into the config.xml file of our module:

<config>
	   <!-- -->
	    <global>
		<!-- -->
		<models>
		    <solvingmagento_orderexport>
			<class>Solvingmagento_OrderExport_Model</class>
		    </solvingmagento_orderexport>
		</models>
		<!-- -->
	    </global>
	    <!-- -->
	</config>

Under the Model folder of our module we place a file called Observer.php. This is a simple model, which does not need to extend any other class. Inside we place a function exportOrder($observer). Its only parameter is an object of type Varien_Event_Observer, which is passed to it from the event.

	class Solvingmagento_OrderExport_Model_Observer
	{
	    /**
	     * Exports an order after it is placed
	     *
	     * @param Varien_Event_Observer $observer observer object
	     *
	     * @return boolean
	     */
	    public function exportOrder(Varien_Event_Observer $observer)
	    {
		$order = $observer->getEvent()->getOrder();

		Mage::getModel('solvingmagento_orderexport/export')
		    ->exportOrder($order);

		return true;

	    }
	}

When intercepting the sales_order_place_after event we get the $order object representing the order from this $observer parameter. Next, we instantiate another model, solvingmagento_orderexport/export, which contain the actual export functionality. While we implement the order export directly in this function it is better to create a specialized model for that. After all, the observer class’s main task is intercept the events – how these events are processed is not of its business. This way we can avoid having a bloated observer, which would contain unrelated functionality making it difficult to maintain.

The export function is implemented in the class Solvingmagento_OrderExport_Model_Export, which we place, naturally, in file called Export.php:

	class Solvingmagento_OrderExport_Model_Export
	{

	    /**
	     * Generates an XML file from the order data and places it into
	     * the var/export directory
	     *
	     * @param Mage_Sales_Model_Order $order order object
	     *
	     * @return boolean
	     */
	    public function exportOrder($order)
	    {
		$dirPath = Mage::getBaseDir('var') . DS . 'export';

		//if the export directory does not exist, create it
		if (!is_dir($dirPath)) {
		    mkdir($dirPath, 0777, true);
		}

		$data = $order->getData();
 
		$xml = new SimpleXMLElement('<root/>');
 
		$callback =
		    function ($value, $key) use (&$xml, &$callback) {
		        if ($value instanceof Varien_Object && is_array($value->getData())) {
		            $value = $value->getData();
		        }
		        if (is_array($value)) {
		            array_walk_recursive($value, $callback);
		        }
		        $xml->addChild($key, serialize($value));
		    };
		 
		array_walk_recursive($data, $callback);

		file_put_contents(
		    $dirPath. DS .$order->getIncrementId().'.xml',
		    $xml->asXML()
		);

		return true;
	    }
	}

The process implemented in the exportOrder function is quite simple. First, we must establish the export path. If it does not exist under var/export (related to the application root, not the module folder), it is created. The data is extracted from the order object as an array and is converted into a SimpleXMLElement object. We generate a file name from the order’s increment ID and place the contents of the SimpleXMLElement object into the export file. Done.

We have the observer to capture events and the model to create export files. What is missing is a way to tell the system that our module has an observer for the event sales_order_place_after. Also, we want to make sure that no matter if the order is placed online by a customer or created in the back-end by a shop administrator – the export file has to be created. This means, we need to register a global event observer. To do that we add the following configuration to the config.xml file:

	<config>
	    <!-- -->
	    <global>
		<!-- -->
		<events>
		    <sales_order_place_after>
			<observers>
			    <solvingmagento_orderexport>
				<class>solvingmagento_orderexport/observer</class>
				<method>exportOrder</method>
			    </solvingmagento_orderexport>
			</observers>
		    </sales_order_place_after>
		</events>
		<!-- -->
	    </global>
	    <!-- -->
	</config>

As you can see, our event configuration is under the node thus ensuring that the observer will capture the event from any part of the application. In the node we have placed the observer class pointer, and under the node – the name of the function responsible for the event capture. We have omitted the node, so the observer is instantiated as a singleton, which is OK with our functionality.

The next requirement is sending an email to the administrator when a customer places his very first order. This means that we have to observe an event happening in the shop front-end. Any order placed in the backend by the administrators themselves must be ignored. We will place the event observer configuration under the node:

	<config>
	    <!-- -->
	    <frontend>
		<events>
		    <checkout_onepage_controller_success_action>
			<observers>
			    <solvingmagento_orderexport>
				<class>solvingmagento_orderexport/observer</class>
				<method>newCustomer</method>
			    </solvingmagento_orderexport>
			</observers>
		    </checkout_onepage_controller_success_action>
		</events>
	    </frontend>
	    <!-- -->
	</config>
 

The event in question is checkout_onepage_controller_success_action, which is thrown by Mage_Checkout_OnepageController class in its successAction() function when the customer is shown the checkout success page. Actually this means that this event will be thrown only in the front-end, and that us placing the observer configuration into the node is unnecessary. But for the sake of doing it “by the book” and for the education purposes, we will stick with this redundancy in our example. In some other situation this configuration approach may be necessary.

We extend our observer model with the new function newCustomer($observer):

	public function newCustomer(Varien_Event_Observer $observer)
	{
	    $orderIds = $observer->getEvent()->getOrderIds();

	    if (!is_array($orderIds) || (!array_key_exists(0, $orderIds))) {
	        return;
	    }

	    $order = Mage::getModel('sales/order')->load($orderIds[0]);

	    if (!$order->getId()) {
	        return;
	    }

	    if (!$order->getCustomerId()) {
	        //send a message only for registered customers
	        return;
	    }

	    $customer = Mage::getModel('customer/customer')->load($order->getCustomerId());

	    if (!$customer->getId()) {
	        return;
	    }

	    $customerOrders = Mage::getModel('sales/order')
	    	->getCollection()
	    	->addAttributeToFilter('customer_id', $customer->getId());
	    if (count($customerOrders) > 1) {
	        // send a message only after the first order
	        return;
	    }

            return Mage::getModel('solvingmagento_orderexport/customer')
	        ->newCustomer($customer, $order);
	}

The $observer object passes an array of “order IDs”, whose first element if the ID of the order we need. We instantiate the order and check it contains a customer ID. If not then it is an order placed by a guest customer and must be ignored. We also check if the customer has more than one order in his history. If yes then the current order is not his first one and the administrator need not to be notified about it. Finally, after all the checks, we instantiate a model solvingmagento_orderexport/customer and call its function newCustomer($customer, $order).

We have to create this new model. Place a file called Customer.php into the Model folder:

	class Solvingmagento_OrderExport_Model_Customer
	{
	    /**
	     * Sends a message to the shop admin about a new customer registration
	     *
	     * @param Mage_Customer_Model_Customer $customer customer object
	     * @param Mage_Sales_Model_Order       $order    order object
	     *
	     * @return boolean
	     */
	    public function newCustomer($customer, $order)
	    {
		try {
		    $storeId = $order->getStoreId();

		    $templateId = Mage::getStoreConfig(
			'sales_email/order/new_customer_template',
			$storeId
		    );

		    $mailer = Mage::getModel('core/email_template_mailer');
		    $emailInfo = Mage::getModel('core/email_info');
		    $emailInfo->addTo(
			Mage::getStoreConfig(Mage_Sales_Model_Order::XML_PATH_EMAIL_IDENTITY)
		    );

		    $mailer->addEmailInfo($emailInfo);

		    // Set all required params and send emails
		    $mailer->setSender(
			Mage::getStoreConfig(
			    Mage_Sales_Model_Order::XML_PATH_EMAIL_IDENTITY,
			    $storeId
			)
		    );
		    $mailer->setStoreId($storeId);
		    $mailer->setTemplateId($templateId);
		    $mailer->setTemplateParams(
			array(
			    'customer'  => $customer
			)
		    );
		    $mailer->send();
		} catch (Exception $e) {
		    Mage::logException($e);
		    return false;
		}

		return true;

	    }
	}

The process in the newCustomer function is as follows:

  • Get the store ID from the order object – we need this information to load correct template and configuration settings.
  • Load a template ID from our email. This setting is saved in the shop back-end configuration under “Sales Emails” -> “Order” -> “New customer template”. We will set up this configuration node and create the template later.
  • Instantiate an object of the Mage_Core_Model_Email_Template_Mailer type. This “mailer” object is responsible for sending the email.
  • Create an “emailInfo” object and add a recipient email address. This address is loaded from the shop configuration under the Mage_Sales_Model_Order::XML_PATH_EMAIL_IDENTITY node, which translates into “Sales Emails” -> “Order” -> “New Order Confirmation Email Sender”, i.e. the address, which is used as a sender of the order confirmation emails. If you need to use up another account, feel free to change this line.
  • Add a template parameter ‘customer’ and assing a value to it – the current customer object.
  • Send the email.

To send this email we have to set up a template. The templates will be stored in the table core_email_template. To get it there we will use an installation script, which is run the first time the shop is started after our module is activated. Add the following configuration to the config.xml:

	<config>
	    <!-- -->
	    <global>
		<!-- -->
		<resources>
		    <solvingmagento_orderexport_setup>
			<setup>
			    <module>Solvingmagento_OrderExport</module>
			    <class>Mage_Core_Model_Resource_Setup</class>
			</setup>
		    </solvingmagento_orderexport_setup>
		</resources>
		<!-- -->
	    </global>
	    <!-- -->
	</config>

Create a file called install-0.1.0.php in the sql/solvingmagento_orderexport_setup directory:

	$installer = $this;

	$installer->startSetup();

	$installer->run(
	    "INSERT INTO `{$this->getTable('core_email_template')}`
	    (`template_code`, `template_text`, `template_type`, `template_subject`)
	    VALUES (
		'New Customer and First Order',
		'A first order by a new customer: {{htmlescape var=\$customer.getName()}}, id: {{var=\$customer.getId()}}',
		'2',
		'A first order by a new customer'
	    )"
	);

	$installer->endSetup();

This script will add the template into the table core_email_template.

We are ready to run the module for the first time. Clear the cache and open the shop URL in your browser. Assuming everything went OK you can proceed to log into the administrative back-end. In the “System” -> “Transactional Emails” page you will be able to find our new template “New Customer and First Order”. Click on it and look into its contents:

transactional_observer

Note the use of the template variable “$customer” – we are able to access the methods of the customer object we pass to the template!

Now go to the “Configuration” page and perform the last settings of the module before we can use its functionality. Under “Sales Emails” -> “Order” select our new template from the drop-down list for the “New Customer Template” parameter. Save the config, clear cache and we are done!

template_setting

Make an order in your front-end, make sure this is the first order for the customer account you are using (choose the option “register a new account” in the check-out’s first step) and see if the order is exported into var/export and if an email notification is sent to the administrator’s address. Create an order is the back-end and see that while the order is exported, no email is sent.

I hope that this tutorial has helped you to understand the ways observers and events are used in Magento. Following the observer pattern is the preferred method of extending the Magento functionality. Events are ubiquitous in Magento. When deciding if a model rewrite is to be done, look if there is an event you can use instead. Events make code more flexible and reduce incompatibility issues between extensions. So, go ahead and use them!

P.S. The finished module can be downloaded here: Solvingmagento_OrderExport.

36 thoughts on “Events and Observers: a Magento Tutorial

  1. Thank you such detailed explanation of the whole process. Observers make way more sense to me now.


    " The local code pool’s package of the module is Solvingmagento and the module name is OrderExport "

    Am I correct thinking, “Solvingmagento” is the Namespace or package, and OrderExport the modulename?

  2. it is very nice explanation presenting concept in simple and nice fashion and also thanks for sharing knowledge

  3. HI
    I am new to Magento . Can you explain me how to place the order.After placing the order how the order details will come to custom_order_detatails.can you please explain me step by step.

  4. I’m getting the following warning: ‘SimpleXMLElement::addChild() expects parameter 1 to be string’ in the Export.php file on the line containing: array_walk_recursive($data, array ($xml, ‘addChild’));

    Any ideas why?
    Thanks!

    • Hi Tim,

      Thank you for pointing out this bug – function array_walk_recursive passes the array element value as the first parameter and the array element key as the second to the addChild() method that does require both parameters to be string. Using the order data export to XML this way produces a warning. I replaced this code with a callback function that should work better:

      $data = $order->getData();
      
      $xml = new SimpleXMLElement('<root/>');
      
      $callback =
          function ($value, $key) use (&$xml, &$callback) {
              if ($value instanceof Varien_Object && is_array($value->getData())) {
                  $value = $value->getData();
              }
              if (is_array($value)) {
                  array_walk_recursive($value, $callback);
              }
              $xml->addChild($key, (string) $value);
          };
      
      array_walk_recursive($data, $callback);
      
      • Thank you Oleg!

        I had to make one small adjustment to your addition. I changed line 13 above to:

        $xml->addChild($key, \serialize($value));

        and it worked afterward.

        Many thanks again!

  5. Hey Oleg,
    Awesome tutorial! I’m excited to go back and read some of your other articles as well. I’m glad I’ve found your site.

    I’m working on an observer similar to what you’ve done here. I’m having a problem instantiating another model. I call it just like you do (but with different names) Mage::getModel('solvingmagento_orderexport/export')
    ->exportOrder($order);
    And I get an error that says Warning: include(): Failed opening 'Mage/[Namespace]/[ModuleName]/Model/Filename.php' for inclusion

    Everything is correct in the include path, except that it stuck a “Mage” folder on the front. Do you have any idea about what I may have done wrong or how to get rid of that?

    Thank you so much.

    • Hi Timothy,

      This kind or error happens when Magento is unable to locate a configuration node for your modules’ models. For example, when calling Mage::getModel('solvingmagento_orderexport/export'), the configuration node name is `solvingmagento_orderexport`. This node must be present in the module’s config.xml file. If there is a typo, and the node name in the config.xml is not the same as in the model call, then Magento is unable to find the package and extension. Magento assumes that this is a core model call and adds ‘Mage’ when constructing the model file path.

      So my advice is: check for typos in the getModel() calls and in the config.xml file. Make sure that the global>models configuration in the config.xml file is correct.

  6. Thanks for this usefull tutorial

    But I have the following error message :

    “‘Cannot send headers; headers already sent in …”

    Please could you help me

  7. After you create the module structure: “We also add a basic module configuration to the etc/config.xml:”

    do you mean app/etc/config.xml or app/code/local/Package/Module/etc/config.xml ?

  8. Regards… with
    “…

    Solvingmagento_OrderExport_Helper

    …” you stablish all the Helpers and with
    “…

    Solvingmagento_OrderExport_Model

    …” You stablish all the models, so you don’t have to set every model you’ve created?

    • Hi Nelson,

      WordPress has stripped the code in your comment, and I can only guess what you are asking.

      If you are referring to the configuration in the config.xml file, then yes, I use these entries to tell Magento where to find my helper and model classes. When I call Mage::helper('orderexport'), Magento will parse the configuration node and use the string ‘Solvingmagento_OrderExport_Helper’ to figure out that I need the class under Solvingmagento/OrderExport/Helper/Data.php.

      Similarly it works with the model configuration: a call to Mage::getModel('solvingmagento_orderexport/export') will translate to file path Solvingmagento/OrderExport/Model/Export.php

  9. Hey Oleg, Thanks for your tutorial. I am working on the same event. However using sales place order event increases the load time. Is it possible to make observer not hold up event. I am sending an sms on this event and in worst case it takes 20 to 30 seconds to load. Ideally, I would want the customer to go to success.phtml while the SMS call is being made.

  10. Hey ,
    i follow the tutorial and the mail not sended

    maybe because i’m on localhost …
    (?)

    and one more question
    if we create the email template- why we run the installer on mysql-0.1.0?

    thanks,
    nir goldman

  11. Thanks for a great post! I have implemented it on my site and it works like a dream.
    Question: I need more information in the xml: The details of the items ordered and the delivery details for the order, etc. The more info, the better! Please help!

  12. Hi Oleg,

    Great tutorial, it’s very informative.

    It works well except for one thing, I don’t receive the e-mail send to the admin. Do you have any idea what’s causing this?

    I am on Magento CE 1.9.0.1.

    Thanks in advance.

    Berend

    • Hi Berend,

      There can several reasons why the email is not being sent. Is your Magento shop able to send emails at all? The SMTP settings may need to be configured first.

      • Thanks for your quick reply.

        Yes, Magento sends e-mails correctly (order, transactional, contact form). I’m inda clueless at this point, but so close to victory…

      • Thanks for your quick reply.

        Yes, Magento sends e-mails correctly (order, transactional, contact form). I’m kinda clueless at this point, but so close to victory…

  13. Hi Oleg,

    You are perfect. This is working like a charm in localhost. But i have problem creating xml file in remote server. I need to change the permission of the folder var/export to “recurse permission of directories” in ftp then only the newly created xml file is displaying. I have already updated the code as chmod recursive permission for var/export dir. Still i cant get the solution. Pl help me as i know this question is apart from this tutorial but i believe you can solve my problem.

    Thanks

  14. Hi Oleg,

    Great article! This strategy is exactly what we need for a client who wants to send a customer email each time a new customer registers, and another one after they place their second order. I especially like the simple way you have incorporated the sql insert statement into the template setup script.

    By the way, I found a small typo:
    Errata:

    The next requirement is sending an email to the administrator when a customer places his very first order. This means that we have to observe oan an event happening in the shop front-end. Any order placed in the backend by the administrators themselves must be ignored. We will place the event observer configuration under the node:

    Thanks

  15. Just a slight correction to avoid confusion. When you say
    “We have omitted the node, so the observer is instantiated as a singleton, which is OK with our functionality.”

    you should specify that it’s the node which is omitted, as stated in the related article “Event-Driven Architecture in Magento: Observer Pattern”.

    Thanks for this post, I’m gonna have to synchronize Magento with Dynamics NAV and I think I’ll make heavy use of observers.

  16. Hi
    I uploaded the files as instructed but nothing is happening, could it be my version of Magento(Community Edition 1.9.3 )?
    I am not getting any errors or any indication at all.
    Please assist.

Leave a Reply

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

Theme: Esquire by Matthew Buchanan.