MVC Developers Part 10 – Magento system overrides and expandability

An often-touted and often congested part of the Magento e-commerce system is the ability to adapt core system behavior. Another hot topic for Magento developers upgradability, and how to prevail in the way of that. Today we look at the different ways to make adjustments switching versions is difficult.

Before we begin, it is important to note that we are talking about changing the core “business logic” of Magento. Changes phtml templates are expected and common in all but the simplest of shops.

Hacking the Source

The “least upgraded” way to conduct Magento (or a PHP-based system to change) is a modification of the source directly. Do you want the behavior of the Product Model, Product Model you edit the file change.

app/code/core/Mage/Catalog/Model/Product.php

When you do this, you have forked the Magento code base. Anytime you upgrade the system you need to do a file by file merge with your forked version. That’s no good.

Also running the risk of changing expected behavior of the methods they return different values, not the measures that the system can depend on, or alteration of data in unexpected (rather than expected), ways. We talk more about this below.

Unfortunately, despite its inadvisability, this is the easiest and most understandable way for many PHP developers to work with Magento. Before starting a new project I always download a clean version of the source and performing a diff against both lib and app / code / core to see what kind of changes have been made in the core.

Including Different Classes

Magento, PHP or rather, looking for a class files in these folders Magento.


lib/* 
app/code/core/* 
app/code/community/* 
app/code/local/*

Because of this, and because of the order under Magento PHP’s built paths, placing a copy of a file in the core app / code / local directory means PHP will first. So if you wanted the functionality of the product model change, would you describe your own copy.


YOURS:    app/code/local/Mage/Catalog/Model/Product.php
ORIGINAL: app/code/core/Mage/Catalog/Model/Product.php

Your file defines the class instead of the nuclear file, and never the core file must be included. This avoids the problem of merging files hacking the source creates and centralizes all your adjustments in a directory structure.

But this is only marginally better, and a solution that you should avoid. As with hacking the core system files, you risk changing the behavior of vital class methods.

For example, consider the GetName Method afformentioned Product Model

/**
 * Get product name
 *
 * @return string
 */
public function getName()
{
    return $this->_getData('name');
} 

Although this method mandatory, you might accidentally add a code path in your transfer, where this method returns null.

/**
 * LOCAL OVERRIDE! Get product name
 *
 * @return string
 */
public function getName($param=false)
{
    if($param == self:NICKNAME)
    {
        return $this->_getData('nickname');
    }
    else if($param == self::BAR)
    {
        return $this->_getData('name')
    }

    //forgot a return because we're working too hard
}

If other parts of the system rely on this method to a string, maybe your customizations to break other parts of the system. It gets worse when methods are objects to return when trying to a method for null call will result in a fatal error (this is part of what Java and C # programmers are talking about w / r / t type safety PHP).

Next, consider the method to validate the same model.

public function validate()
{
    Mage::dispatchEvent($this->_eventPrefix.'_validate_before', array($this->_eventObject=>$this));
    $this->_getResource()->validate($this);
    Mage::dispatchEvent($this->_eventPrefix.'_validate_after', array($this->_eventObject=>$this));
    return $this;
}

Here you accidentally delete the dispatching events.

//My local override!
public function validate()
{
    $this->_getResource()->validate($this);
    $this->myCustomValidation($this);
    return $this;
}

Other components of the system, which is fired at these events would stop working.

Finally, you’re still not out of the forest during an upgrade. If the methods of a class change during an upgrade, Magento will still including your old and outdated class with the old, outdated methods. Practically speaking, this means you still need a manual merge to perform during your upgrade.

Using the Override/Rewrite System

Magento’s class overwrite / Rewrite system is based on using a factory pattern to create models, helpers, and Blocks. If you say

Mage::getModel('catalog/product');

tell Magento

“Hey, go lookup the class to use for a” catalog / product “and instantiate it for me.”

In turn, consult the Magento system configuration files and says:

“Hey, config.xml tree! What class should I use for a” catalog / product?

Magento instantiates and then returns the model for you.

When a class on writing Magentoo, change the configuration files say

“Hey, as a” catalog / product design has been instantiated, using my class (Myp_Mym_Model_Product) instead of the core class

Then, when your class definition, you have to renew the original class

class Myp_Mym_Model_Product extends Mage_Catalog_Model_Product
{
}

This way, your new class has all the old functionality of the original class. Here you can avoid the problem of merging files during an upgrade and the problem with obsolete methods of your class after an upgrade.

However, there remains the matter of changing behavior method. further examine the GetName and validate methods. Your new methods can be as easy to forget / change-the type of return value, or a critical piece of functionality in the original methods to leave.

class Myp_Mym_Model_Product extends Mage_Catalog_Model_Product
{
    public function validate()
    {
        $this->_getResource()->validate($this);
        $this->myCustomValidation($this);
        return $this;
    }   

    public function getName($param=false)
    {
        if($param == self:NICKNAME)
        {
            return $this->_getData('nickname');
        }
        else if($param == self::BAR)
        {
            return $this->_getData('name')
        }

        //forgot a return because we're working too hard
    }
}

The overwrite / rewrite the system will not protect you against this, but it gives you ways to prevent this. Because we actually extend the original class, we call the original method using PHP’s parent:: construct

class Myp_Mym_Model_Product extends Mage_Catalog_Model_Product
{
    public function validate()
    {
        //put your custom validation up here
        return parent::validate();
    }   
    public function getName($param=false)
    {
        $original_return = parent::getName();
        if($param == self::SOMECONST)
        {
            $original_return = $this->getSomethingElse();
        }
        return $original_return;
    }
} 

By calling the original methods, you ensure that all actions that take place will take place. Also by retrieving the original value to return, you’re less likely that your method will return something unexpected.

This is reduced, not eliminated. It remains to you as the developer to ensure that your custom code return objects or primitives which are the same as the original method’s. That means that even if you override the supplied system use, it is still possible to break the system.

Because of this, when I control the architecture of a solution, I try my overrides to a minimum. When I have to write about I always try my methods end with a

return parent::originalMethod();

If my adjustments necessary for the original method for the first run, I used a similar construction

public function someMethod()
{
    $original_return = Mage::getModel('mymodule/immutable')
    ->setValue('this is a test');

    //my custom code here
    return $original_return->getValue();
}

The ‘mymodule / immutable’ URI refers to a simple immutable object implementation.

class Alanstormdotcom_Mymodule_Model_Immutable
{
    protected $_value=null;
    public function setValue($thing)
    {
        if(is_null($this->_value))
        {
            $this->_value = $thing;
            return $this;
        }

        //if we try to set the value again, throw an exception.
        throw new Exception('Already Set');
    }

    public function getValue()
    {
        return $this->_value;
    }
}

This does not prevent anyone (including myself) overwrite $ original_return with something else, but discouraging and makes it obvious when someone has. If there is a method of writing in my class that does not end in one of two $ original_return-> getValue (), or parent:: method for my eyes are immediately drawn to it as a possible culprit for that problem I’m debugging.

Finally, there are times where you want (or do you want) to return value of a key method to change. If the need arrises of this, it is much safer to provide a new method that the original interviews, and then you change the theme of this new method.

class Mage_Catalog_Model_Original extends Mage_Core_Model_Abstract
{
    protected function getSomeCollectionOriginal()
    {
        return Mage::getModel('foo/bar')
        ->getCollection()->addFieldToFilter('some_field', '42');
    }
}

class Myp_Mym_Model_New extends Mage_Catalog_Model_Original
{
    public function getSomeCollectionWithAdditionalItems()
    {
        $collection = $this->getSomeCollectionOriginal();
        //now, alter or override the $collection with
        //your custom code

        return $collection;
    }
}

This provides additional effects created by the original method still occur, the original return type / result is the same, and you can use your custom logic continues to add specific components of the system.

Wrapup

Upgradability is a large or even small version of a software package that you can not control is always going to be a bumpy ride. Apple and Microsoft spend millions of dollars of testing their upgrade paths to new OS releases, and the Internet is still filled with horror stories of the peripheral issues which they lack. Even in organizations where everyone is working towards the same goal often unspoken assumptions updates on the surface quickly builds and developers are forced to break the reality that they work for other people to recognize.

As a user of the Magento e-commerce system, your job is to ensure that changes to the core of the system are minimized, and when these changes are necessary they are ready in a clean, easily diagnosable matter. Using the Magento overwrite / rewrite system is a powerful tool in that direction.

(Based on Alanstorm Turtorial)

Comments

Leave a Reply

Your email address will not be published.

five + twelve =

Security Code: