Showing posts with label Observer. Show all posts
Showing posts with label Observer. Show all posts

Saturday, April 27, 2013

Magento observer class

One of the parameters of a Magento observer is the 'class'.

For example, the Magento wiki has an example of customizing Magento using an observer. This example includes configuration of the module that provides the observer:

    <?xml version="1.0"?>
    <config>
      <global>
        <models>
            <xyzcatalog>
                 <class>Xyz_Catalog_Model</class>
            </xyzcatalog>
        </models>
        <events>
          <catalog_product_get_final_price>
            <observers>
              <xyz_catalog_price_observer>
                <type>singleton</type>
                <class>Xyz_Catalog_Model_Price_Observer</class>
                <method>apply_discount_percent</method>
              </xyz_catalog_price_observer>
            </observers>
          </catalog_product_get_final_price>     
        </events>
      </global>
    </config>


I wondered why, in all the examples I found, the observer class was always a 'Model' class. It seems strange to me that a 'Model' (think MVC) would handle an event. I think of a Model as getting or persisting data. A controller seems a more appropriate component for handling an event. But, again, every example I have seen executes a method from a Model class. So, I had a look at the Magento code to see what was going on and whether there were any clues as to why a Model rather than a Controller.

In some cases, like the example above, the class name is given explicitly. In every case I have seen, the class name includes 'Model'. Note that in a case like this example, the Mage __autoload function changes '_' to directory separator so, on a Linux system, that class would be loaded from Xyz/Catalog/Model/Price/Observer.php. In this case, it is just a class and needn't be a Model as far as I can tell.

In other cases, the class is specified differently: as 'module/model'. In this case, the processing within Magento inserts 'Model' into the class name, so it is a bit more explicitly a Model class. See getGroupedClassName in Mage_Core_Model_Config for the full details. getGroupedClassName is called to transform the class name if it contains '/', in which case the class becomes getGroupedClassName('model', $class).


Magento observer types

When configuring an Observer in Magento, one of the configuration parameters is 'type'.

For example, the Magento wiki has an example of customizing Magento using an event observer, where the module configuration is:

<?xml version="1.0"?>
    <config>
      <global>
        <models>
            <xyzcatalog>
                 <class>Xyz_Catalog_Model</class>
            </xyzcatalog>
        </models>
        <events>
          <catalog_product_get_final_price>
            <observers>
              <xyz_catalog_price_observer>
                <type>singleton</type>
                <class>Xyz_Catalog_Model_Price_Observer</class>
                <method>apply_discount_percent</method>
              </xyz_catalog_price_observer>
            </observers>
          </catalog_product_get_final_price>     
        </events>
      </global>
    </config>


Note the 'type' key in the configuration. In this case, the content is 'singleton', but there is no explanation of what this aspect of the configuration is about.

The type is dealt with in the dispatchEvent method of class Mage_Core_Model_App, method dispatchEvent, which is executed from class Mage, method dispatchEvent (the latter seems to be what is executed generally throughout the code but it is just a thin wrapper around the former). This function ends with a loop that executes each observer registered for the event in turn, as follows:

            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);
            }
Note that there are really only three cases for type: 'disabled', in which case no observer is called; 'object' or 'model', which are equivalent, in which case Mage::get_Model is called; or any other values (any other value is equivalent - this includes 'singleton'), in which case Mage::getSingleton is called. Mage::get_Model and Mage::getSingleton are both passed the value of the 'model' parameter of the observer configuration.

Mage::getSingleton($class) calls Mage::getModel($class) but it caches the return value and calls Mage:;getModel only once. Subsequent calls for the same $class return the cached instance rather than a new instance. In contract, Mage::getModel($class) returns a new instance of the class every time.

So, for new modules, one might use the types: 'disabled', 'model' or 'singleton'.

Type 'disabled': the observer class is not instantiated and the observer method is not executed.

Type 'model': a new instance of the observer class is instantiated for each event and the observer method of that instance is executed.

Type 'singleton': a single instance of the observer class is instantiated and the observer method of that single instance is executed for each event.

Tuesday, April 23, 2013

Magento observer method arguments - what are they?

One of the difficulties I have developing event observers for Magento is that I don't know what the arguments to the observer methods are. Most examples show a single argument, typically named $observer. What is it and what are its methods and attributes? I have had difficulty finding this out.

Initially I dumped the argument to a log using print_r:

        public function observer_method($observer) {
            Mage::log(
                "Observer observer_method executing with: " .
                print_r($observer,true),
                null,
                'MyModule.log'
            );
        }

This worked fine for a while, then I tried this with the argument passed for the sales_order_item_after_save event and quickly ran out of memory. The problem is that the passed object has cyclical links and print_r doesn't notice the recursion: it just keeps printing until it runs out of memory. The var_dump function has the same problem.

Fortunately, most objects in Magento are derived from the Varien_Object class and this class has a debug() method which handles recursion. So, a more general solution to inspecting data in Magento is to combine print_r with debug:

        public function observer_method($observer) {
            Mage::log(
                "Observer observer_method executing with: " .
                print_r($observer->debug(),true),
                null,
                'MyModule.log'
            );
        }

The debug method returns an array with no recursion and print_r renders that to a string.

Another approach is to find where Mage::dispatchEvent is executed for the event of interest and examine the arguments that are passed. This is easy for some events: Mage::dispatchEvent is called with the event name as a literal argument. One can grep the source for these. But, again, the case of sales_order_item_after_save was more challenging. Grepping the source for this event yielded nothing except observers. Eventually I grepped for 'sales_order_item' and 'after_save' separately and found where the event might be dispatched...

In app/code/core/Mage/Core/Model/Abstract.php one finds:

    /**
     * Processing object after save data
     *
     * @return Mage_Core_Model_Abstract
     */
    protected function _afterSave()
    {
        $this->cleanModelCache();
        Mage::dispatchEvent('model_save_after', array('object'=>$this));
        Mage::dispatchEvent($this->_eventPrefix.'_save_after', $this->_getEventData());
        return $this;
    }

So, since I can't find it elsewhere, I'm guessing whatever issues the sales_order_item_save_after event is calling _afterSave() with _eventPrefix set to 'sales_order_item'.

I found one class (Mage_Sales_Model_Order_Item in app/code/core/Mage/Sales/Model/Order/Item.php) that extends Mage_Core_Model_Abstract and sets a property _eventPrefix to 'sales_order_item'. It doesn't call _afterSave itself, so I still don't know exactly what the passed arguments are, but getting closer. The Mage_Sales_Model_Order_Item class doesn't have a _getEventData() method, so it is most likely that _getEventData from class Mage_Core_Model_Abstract is the culprit.

From Mage_Core_Model_Abstract:

    /**
     * Get array of objects transfered to default events processing
     *
     * @return array
     */
    protected function _getEventData()
    {
        return array(
            'data_object'       => $this,
            $this->_eventObject => $this,
        );
    }

and

    /**
     * Parameter name in event
     *
     * In observe method you can use $observer->getEvent()->getObject() in this case
     *
     * @var string
     */
    protected $_eventObject = 'object';




But, class Mage_Sales_Model_Order_Item has:

    protected $_eventObject = 'item';

So, the argument to the event observer for sales_order_item_save_after should be an array with two elements: 'data_object' and 'item', but both referring to the same data: the Mage_Sales_Model_Order_Item instance.

Maybe next time I'll try generating a stack trace in the observer method. That should help to pin down the method that dispatches the event quickly.


Labels