Search This Blog

Monday, February 21, 2011

Writing a Custom Magento Module

The aim of this tutorial is to provide a good reference guide to keep with you when developing custom modules with ecommerce solution Magento. It details some of the key sections of how to build a custom Magento module, some of the concepts, assumptions and key points that that you are likely to encounter on a regular basis when building ecommerce installations with this software.

While Magento does not have a great reputation for documentation, there are some good resources available, including the Magento Wiki and php|architect’s Guide to Programming Magento. I used both of these when working for Session Digital as part of Ibuildings and I highly recommend them to you, along with the Magento Forums. Looking through these splintered resources can be time-consuming so in this article I give you the key details when building a module in Magento.

To accompany this article, you will find at the end of the page series of recommended resource links, and a download containing the example module that is built in the examples shown, available in a variety of formats. Hopefully these will help you continue to develop successfully with Magento!
The Magento Way

For those, seasoned in customising or maintaining other people’s software, you know that each has its own approach to things. You may not always agree with it or understand the logic behind it, but if you are going to work with it, you need to know what it is unless you want to go grey prematurely!

In this vein, before you make your next, or first, Magento module, it is helpful to understand some key concepts in how Magento is constructed. Magento is based on the Zend Framework and follows a simple code discovery, or resolution order, when compiling. There are three directories in which it finds code. These are core, community and local. Firstly, the core code modules are considered. Secondly, if code in the community modules are found that supercede it, they are used. Finally if code is found in the local directory, that supercedes the community directory, this is used. Said another way, code modules that you develop and place in the local directory, have priority over community and the Magento core.

This has a number of benefits from a developmental point of view. Chief amongst these is to allow for a seamless upgrade path. You can develop new modules, integrate third-party modules from the community in to your install and know, with a reasonable amount of confidence, that when you upgrade to the next release of Magento your hard work will survive intact. So remember never to touch the core code as that can break, or severely hamper, future upgrades. Too often I see in the forums, complaints about upgrades breaking, yet references to changing or hacking core code.

This philosophy of extending existing functionality means that you can take a community module to achieve the majority of your requirements and then supplement the missing functionality with a local module. You get a core of the work and only update where it’s missing what your project requires. Working in this way keeps development time and costs down, which is always good!

If you keep these things in mind whilst developing, it will help provide a simpler, more hassle-free development process as well as a more maintainable end-product.
Directory Structure

We have covered the ethos of Magento so we shall start getting our hands dirty and cover the filesystem structure of a standard module, by creating a our own module. Let’s say that you want to spruce up your Magento shop by combining your Flickr feed of customers using your products on the front page of your site. What we will do in this tutorial is create a simple module that links to an existing Flickr account and render a configurable set of images.

To start us off we will assume the following:

    * Your Company: Widgets Inc
    * Your Module: FlickrFeed
    * You have a Flickr account with the accompanying API key

Key Points

The most important parts of a module are these:

    * app/code/local/
    * app/etc/modules/

In app/code/local you will store all your modules. In app/etc/modules, you store your module specification file. The specification file is essential as it tells Magento that your module exists, making it available to use and configure. The local directory is where you will start to build your module repository. Analagous to PHP namespaces, C++ includes, or a Java package, the local directory provides a clear, professional and consistent method of organising and distributing your modules.
Key Files

app/Mage.php This file cannot be underestimated when developing with Magento. It provides a series of static methods that make working with Magento much more straightforward. There is some debate about the merits of this approach, but when working with it, these utility methods make life simpler. Some common methods you can find here are:

Mage::log Access the system logger

Mage::getModel Access a system model object

Mage::getStoreConfig Access the system configuration settings
Standard Module Layout

All modules follow the same structure. I’ll illustrate it using the company and module outlined above:

WidgetsInc/FlickrFeed/
  Adminhtml (Contains  the configuration for the admin menus)
    Model
      Block (Contains front-end output)
        Admin
  controllers (Controllers for interacting with your module)
  etc (The main configuration directory)
    config.xml (Configuration options and default settings for your module)
    system.xml (Administration settings and admin form options for the module)
  Helper (Helpers for the module, covering areas such as translation)
    Data.php
  Model
    Mysql4 (The top ORM model directory)
     
    Observer.php (Observers able to respond to system events)
  sql
    _setup (Contains setup and update scripts for your module)

In order to build the FlickrFeed module, we’ll need to add a number of files. The following sections will demonstrate each of these in turn.
config.xml

This is the core config file for your module. A minimalist version is available in the example download which you can find at the end of this post. Now let’s take you through what it means.

Here is the core definition of your module.

    * version is essential as it links in with the install and upgrade scripts.
    * depends lists the dependencies of your module. If the dependencies here are not satisfied, as you would assume, your module will not be enabled.
    * codePool specifies in what resource your module is located.

As you can assume, if these aren’t satisfied, the module will not load.

The models section sets up the data source connections and defines how to refer to it.

The resources section provides access to the system read, write and setup functionality for the module. If this section is not present, your module will be able to do very little.

The adminhtml section links the module in to the admin functionality, such as forms and configuration setups.

The routers section sets up the routes so that you can access the module. Unsurprisingly, default set up the defaults for your module that can be accessed as required.
system.xml

This file allows you to define an admin form for setting the key options for the feed. If you have a look at the one contained in the sample module, you’ll see an element groups. These enble you to combine the fields to display in to logical groupings, which will be rendered in a fieldsets containing the form field elements.

After the groups, the next element provides the initialisation of the section and then under the element fields, you list the fields that are to be displayed in this grouping. Fields and fieldsets can be disabled by default, or specifically in the website and/or store through show_in_default, show_in_website and show_in_store. To auto-translate fields, list them in the attribute translate for each group or selection element.
Managing Model Information

Next we need to look at the model files that will all us to work retrieve information from the module. These files are:

Model
  Feed.php
  Mysql4
    Feed.php
    Feed/
      Collection/
      Collection.php

In Model/Mysql4/Feed.php is the main model class, extending the core model class, making all of its functionality available. Model/Mysql4/Feed/Collection.php overrides the core model collection class, as with Feed.php, makes all the collection functionality available.
Working with the Magento Data Model

Now for the following example, let’s assume that we have a table structure similar to the following:
Field     Type     Null     Key     Default     Extra
address_id     int(10) unsigned     NO     PRI     NULL     auto_increment
address1     varchar(50)     NO         NULL    
address2     varchar(50)     NO         NULL    
city_id     int(10) unsigned     YES         NULL    
state_county     varchar(100)     YES         –    
post_code     varchar(30)     NO         NULL    
country     varchar(50)     YES         uk    
pobox     varchar(50)     YES         –    
created_date     datetime     NO         NULL    
last_modified     datetime     NO         NULL    

During a normal interaction with your model, you’re likely to retrieve, persist (save/update) and delete information from that model. So let’s look at how you can do this with Magento.
Retrieving Data

// get a handle on all the available feeds
$collection = Mage::getModel('flickrfeed/feed')->getCollection();
if ($collection->count() >= 1) {
    // store the retrieved feeds
    $feedOutput = array();
    // iterate over the retrieved retrieved collection
    foreach ($collection as $flickrFeedItem) {
        $feedOutput[] = $flickrFeedItem->getData();
    }
}

Filtering the information

Now what if you did not require all the available data at once? There are so many situations where you do not want every record available. Magento has plenty of ways of filtering data but for this example we’ll use: addFieldToFilter(<field_name>, <field_value>).

$collection = Mage::getModel('flickrfeed/feed')->getCollection()
                               ->addFieldToFilter('post_code', 'SE11');

After adding to our previous call to getCollection, we have filtered down our returned records to just those with a post_code value of SE11. With the fluid interface we can call this as many times as we need to in order to meet our filtering requirements.
Persisting Data

We will also need to save data and update records, so here is a simple code example to illustrate:

Mage::getModel('flickrfeed/feed')->setData($data)->save();

In this instance, you’re calling the setData method on your model, which itself extends Mage_Core_Model_Mysql4_Abstract. The $data variable will be an associative array where the keys are the names of the columns in which you want the data saved. Then, using the fluent interface, further call the save() method and the data will then be saved by your model. Now, like all OOP classes, you don’t need to accept the parent setData() and save() methods; you can override these should you wish to. It is likely that the default may be quite acceptable in the majority of cases however.
Deleting Data

Now, let’s look at deleting records. Have a look at the code snippet below.

// get a handle on all the available feeds
$collection = Mage::getModel('flickrfeed/feed')->getCollection();
if ($collection->count() &gt;= 1) {
    // iterate over the retrieved retrieved collection
    foreach ($collection as $flickrFeedItem) {
        try {
            $flickrFeedItem->delete();
        } catch (Exception $e) {
            // note that the item couldn't be deleted.
            Mage::log(
            sprintf("Couldn't delete record. [%s]", var_export($_item, TRUE)),
            Zend_Log::ERR
            );
        }
    }
}

This example shows a simple way to delete records. After retrieving a collection, iterate over it using foreach and call delete on each item. Assuming that the method does not throw an exception, your records are deleted. We can also make things easier by creating a method for bulk deletion of records. In the model class, in our case FlickrFeed_Model_Mysql4_Feed, add a deleteAll() method similar to below:

      public function deleteAll()
      {
          // note that the item couldn't be deleted.
          Mage::log("Attempting to clear all records", Zend_Log::INFO);
          // get a handle on all the available feeds
          $collection = Mage::getModel('flickrfeed/feed')-&gt;getCollection();
          // attempt to delete all of them
          // not the most ideal way.
          foreach ($collection as $item) {
              try {
                  $item-&gt;delete();
              } catch (Exception $e) {
                // note that the item couldn't be deleted.
                Mage::log(
                  sprintf("Couldn't delete record. [%s]", var_export($_item, TRUE)),
                  Zend_Log::ERR
                  );
              }
          }
      }

Now all you need to do is call:

Mage::getModel('flickrfeed/feed')->deleteAll();

Logging

Since delete() can throw an exception if something goes wrong, this is a good time to show another Magento utility method: log. As you would assume, this gives quick access to the system log. Just pass in the message that you want logged and the log level, exactly as you would when working with Zend_Log, upon which this is based. Mage has a number of other methods, including getBaseUrl(), getStoreConfig(), getVersion() and dispatchEvent() that I highly encourage you to review.
Installing and Upgrading Modules

As mentioned earlier, one of the key aspects of modules is the value of version in /etc/config.xml. Whenever Magento looks at your module this value is checked; usually when you look at Admin -> system -> configuration -> advanced and click ‘Save Config‘.

The version value is compared against the value of core_resource.version in the database, where core_resource.code matches the name of your module. If the value in the file is higher than in the table column, then Magento looks for files in sql/flickrfeed_setup/ with a filename being equal to or higher than that of that found in config.xml.

If you want to re-run setup/upgrade routines during testing or migration, simply remove references to your modules in core_resource and then, in ‘advanced‘, click ‘Save Config‘ again. Magento will run all your scripts again.

We can now create the files for managing install and updates. Under your module, create the following file structure:

sql

  flickrfeed_setup

  mysql4-install-0.1.0.php

At this point you have the basis of your module. It will install and you will be able to call it!
Automation with Cron

Magento honours the tradition of using tools to use jobs so we you don’t have to by linking to Cron on UNIX based systems. For those unfamiliar with this utility, it is a UNIX daemon that wakes every minute, checks if it is time to run a job, if so, spawns them, and returns to sleep, waking a minute later to run all over again.

To get access to this automation in Magento, have a look at the snippet from config.xml listed below.

<crontab>
  <jobs>
    <publish_product_photos>
      <!-- run every 2 minutes for testing purposes -->
      <schedule><cron_expr>*/2 * * * *</cron_expr></schedule>
      <run><model>flickrfeed/observer::publishProductPhotos</model></run>
    </publish_product_photos >
  </jobs>
</crontab>

Place this snippet inside the root level of config.xml. This will cause flickrfeed/observer::publishProductPhotos to be executed every two minutes. For maintenance sake, we’ve labeled the task publish_product_photos. Inside that module, you can put in any Magento/PHP code that you need to automate. You can imagine that in this function, you could be performing actions such as checking for product photos that have not been published and publishing them.
Observing Application Events

Working with Magento can involve working with a lot of other code, whether that’s the Magento core, or third-party modules. To extend this is not always feasible and can be unecessary, leading to increased development overhead. Why not save your time and development budgets by simply listening for the events occurring and respond when they occur? Your module has the benefits of being self-contained and extremely loosely coupled. You are not necessarily bound to make changes when the module maintainer, or Varien, updates their code.

Now, let’s look at how we would observe an event. Magento calls events via the Mage::dispatchEvent. Given that there is not a comprehensively documented events list, here’s a few examples.

/app/code/core/mage/

    * ./Sales/Model/Order/Invoice.php:
          o sales_order_invoice_pay
          o sales_order_invoice_cancel
    * ./Sales/Model/Convert/Quote
          o sales_convert_quote_to_order
          o sales_convert_quote_address_to_order
          o sales_convert_quote_address_to_order_address
          o sales_convert_quote_payment_to_order_payment
          o sales_convert_quote_item_to_order_item
    * ./Sales/Model/Order.php
          o sales_order_place_after
    * ./Catalog/Model/Convert/Adapter/Product.php
          o catalog_product_import_after

To find a comprehensive list, you can run the following from the shell:

grep -rn Mage::dispatchEvent app/code/core/* --include='*.php'

You could refine this to make the output simpler to read if you wanted to. Alternatively, just search for Mage::dispatchEvent in your favourite editor. In our example we can observe catalog_product_import_after. Below is a snippet from config.xml.

<events>
  <!--listen for the product save action-->
  <catalog_product_import_after>
    <observers>
      <update_flickr_feed>
        <type>model</type>
        <class>FlickrFeed_Model_Observer</class>
        <method>publishCategoryPhotos</method>
      </update_flickr_feed>
    </observers>
  </catalog_product_import_after>
</events>

Put this section inside config -> adminhtml in /etc/config.xml of your module. When the catalog_product_import_after event is called, the system will call FlickrFeed::publishCategoryPhotos. What could be simpler? In your module’s method, you can put any valid Magento/PHP code that suits your needs. In this case, you could use access to the FlickrApi to publish a selection of photos from the latest category that you’ve just added.

All you need to do is determine the event you want to listen for, determine if there’s a suitable system event thrown and setup your module in the above fashion.
Deployment

When you are ready to deploy the module from your local development to your production server, just take the module directory you have built and copy it to the same directory on your production server. One word of caution, if you’re moving from one version of Magento to another, or from Community Edition to Enterprise, you may encounter some issues. So to minimise your deployment issues, aim to use for your development platform an edition and version of Magento as close as possible to your live version.
Download the Sample Code

We provide for you a zip file containing the FlickrFeed Codefor you to download and try out if you would like to.

Magento Connect

Provided by Magento, Magento Connect is where you can find a large and diverse range of modules, or extensions, to Magento. They allow you to quickly and simply extend your installation with new payment gateways, Google Maps integration and a lot more. Modules come in two flavours, free or commercially paid for, with varying support options available.

There is not much difference between the two formats. The modules are all hosted on magento-connect, but when a module is offered commercially, the seller, not Varien, handles payment. A magento-connect account is required, through which you can manage the details of the module.

No comments:

Post a Comment