Saturday, April 27, 2013

Magento object initialization (_construct Vs __construct)

Some classes in Magento define function _construct, some define __construct, some define both and some define neither. What's going on?

The function __construct is executed by PHP when an object is instantiated, if it exists, but function _construct is not. To PHP there is nothing special about _construct.

Initialization in Magento seems to be quite variable. Many classes define __construct. Many, but not all of these, execute parent::__construct - some before and some after local initialization.

In a few cases, __construct ends with $self->_construct(). The Varien_Object class is an example. In these cases, sub-classes can define _construct, in which case the parent's __construct will run and the sub-class's _construct will be run.

Class Varien_Object defines function _construct with the following comment:
    /**
     * Internal constructor not depended on params. Can be used for object initialization
     */

Only a few of the sub-classes of Varien_Object define function _construct. I haven't reviewed them exhaustively, but a significant majority of those I reviewed did not define function _construct.

Class Mage_Core_Block_Abstract also defines function __construct to execute $self->_construct(), and defines function _construct with the comment:

    /**
     * Internal constructor, that is called from real constructor
     *
     * Please override this one instead of overriding real __construct constructor
     *
     */
At least in this case there is a clear preference stated regarding __construct, but still not rationale.

On the other hand, at least one sub-class of Varien_Object (Mage_Core_Model_Email) defines function __construct and does not execute parent::__construct(). Nor does function __construct in class Mage_Core_Model_Email do the equivalent of function __construct in class Varien_Object.  So, the function __construct in class Varien_Object is not always executed in sub-classes of Varien_Object.

Sub-classes defining __construct and executing parent::__construct() seems to be the most common. It is hard to imagine any advantage of using function _construct(), as with Varien_Object. It has been suggested that it makes it less likely to forget to execute parent::__construct(), but this hardly seems likely. One would have to remember to define function _construct rather than function __construct. If one can remember this, then surely one can remember to execute parent::__construct(). It would be a more credible explanation if the use of function _construct were common: it might become habitual or one were not otherwise familiar with PHP. But in fact, classes using function _construct are uncommon. The vast majority of __construct functions do not execute $self->_construct().

Keep in mind that if a sub-class of a class that defines function __construct to execute $self->_construct() defines function _construct and a sub-class of that sub-class also defines function _construct, then when the root class executes $self->_construct, only that in the sub-sub-class will be executed: the function _construct of the intermediate class will not be executed, unless the sub-sub-class executes parent::_construct().
I haven't reviewed the classes of Magento exhaustively, but I have reviewed a significant number and thus far have seen only one (Mage_Core_Block_Template) where function _construct executes parent::_construct().

If sub-sub-classes are going to execute parent::_construct(), it seems to me that it would be simpler to simply use __construct and parent::construct() in the way that would be obvious to any PHP programmer, and do away with function _construct. But that's just my opinion.

In summary, in a few classes function __construct executes $self->_construct() and some sub-classes of these classes define function _construct rather than function __construct. I know neither the motivation nor the benefit of this, particularly as it is so uncommon. It is certainly not the norm in Magento. There has been some discussion on the Magento forums, but no explanation from a core team developer that I have seen.


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.

Thursday, April 25, 2013

Magento installation layout

I inherited Magento in a BitNami image. These are just my notes as I familiarize myself with Magento - probably not very reliable as I don't know much about it yet. Judging by the RELEASE_NOTES.txt file in htdocs, it is version 1.7.0.2. This is the 'Full Release' version of Community Edition currently available from the Magento Commerce download page. I don't know about the BitNami image version, but it too is quite recent.

Magento is installed to /opt/bitnami/apps/magento. It is accessed via an Apache virtual server, configured in /opt/bitnami/apache2/conf. The document root folder is /opt/bitnami/apps/magento/htdocs. Almost the entire Magento installation is within the htdocs folder.

Within /opt/bitnami/apps/magento there are:
  • conf (folder)
  • htdocs (folder)
  • licenses (folder)
  • scripts (folder)
  • updateip.backup (file)
The conf folder contains three files. One relates to Beetailer, which I assume relates to this Facebook integration software. Another is a bit of Apache configuration, which is included into the main Apache configuration in /opt/bitnami/apache2/conf/httpd.conf: this sets up aliases and directory permissions for the Magento installation but does not define the virtual website. Note that the permissions here are wide open for the htdocs folder, but there are .htaccess files that restrict permissions, so it is not a bad as it looks initially. Finally, there is magento_info.txt which contains an encryption key. This may be from BitName. It is vaguely described in their FAQ.

The htdocs folder contains the bulk of the Magento installation. More on that later.

The license folder just has a copy of the license under which the Magento Community Edition software is released.

The scripts folder contains a single sql script (beetailer.sql) which appears to be related to setup of the Beetailer software noted above.

Finally, updateip.backup. This file appears to be part of the BitNami image. It is renamed so that it doesn't run. According to the BitNami Magento FAQ, it is for updating the Magento configuration with the server IP at system boot. Advice for WordPress is to rename it, appending '.backup' to the name, if the server has already been configured. So, it seems a reasonably safe assumption that this file is not doing anything. It is an ELF executable, none the less.

The htdocs folder contains the installation of Magento, almost in its entirety.

Perhaps a good place to begin is with htdocs/.htaccess. This file limits access to the folder, which otherwise is wide open, and provides configuration for various Apache modules. Among other things, it defines the default directory index as 'index.php'.

There are five main php scripts:
  1. api.php
  2. cron.php
  3. get.php
  4. index.php
  5. install.php
The api.php script handles requests to the Magento API. It is part of the Mage_api2 package. I don't know anything about this yet.

The cron.php script handles routine processing. I understand this should be executed on a regular schedule (perhaps daily???) and, possibly among other things, is needed to maintain the Magento cache.

The get.php script appears to be for downloading resources. Again, I don't know anything about this, but at a quick read it looks like it handles returning resources either from the file system or the database.

The index.php script is the main Magento entry point. It handles routine access to the Magento site / store. This script itself is quite brief. It does a little initialization, loads the main Mage script and executes Mage::run().

The install.php script is part of an installation option. Rather than copy the entire Magento installation to the server, it is possible to copy a small subset, including install.php, the downloader folder, which contains code supporting download and installation of Magento, and, no doubt, some other bits of configuration (contained in a relatively small zip file available for download, I believe), then access install.php from the browser to downlaod and install the full Magento release, assuming the web server has sufficient permissions on the htdocs folder.

The app folder contains most of the Magento 'application'.

The downloader folder contains script and configuration for downloading and installing Magento, supporting the install.php script.

The errors folder contains script and resources for handling errors and returning error messages.

The includes folder contains just one file: config.php. In a default install, config.php does nothing. It has lines to set PHP named constants COMPILER_INCLUDE_PATH and COMPILER_COLLECT_PATH, but these are commented out.This relates to the Magento compiler, which can be used to assemble all the class files into a single folder. This compiler is described by Alan Storm, who has many excellent posts about Magento - well worth reading.

The js folder is misleading. It does contain a little javascript, but it also contains css files, various image files and more. I guess it contains the client side parts of the Magento application.

The lib folder contains various libraries and resources used by the Magento application (or constituting the Magento application, depending on how you look at it).

The media folder contains some image files, xml files and various cryptically named files. No idea about this yet.

The nbproject folder contains very little. There are references to Net Beans. No idea about this...

The pkginfo folder contains some 'metapackage' information files. Part of how Magento is packaged and distributed, I imagine.

The shell folder contains a few php scripts that are part of the Mage_Shell package, whatever that is.

The skin folder contains more css and image files. Presumably these are related to styling the Magento interface.

The var folder contains various more or less temporary files, including the Magento cache. Presumably this is mostly managed by the Magento application.

Otherwise, there are license files, release notes and a robots.txt file.

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