10 Magento System Overrides and Upgradability

piaoling  2011-05-03 15:23:31

An oft-touted and often overused feature of The Magento Ecommerce System is the ability to override core system behavior. Another oft-discussed topic for Magento developers is upgradability, and how overrides get in the way of that. Today we’re going to look at the various ways overrides make switching versions difficult.

Before we begin, it’s important to point out we’re talking about changing the core “Business Logic” of Magento. Changes to phtml templates are both expected and common in all but the simplest of stores.

Hacking the Source

The “least upgradable” way to change the behavior of Magento (or any PHP based system) is to alter the source code directly. If you want to change the behavior of the the Product Model, you edit the Product Model file

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

When you do this, you’ve forked the Magento code base. Anytime you upgrade the system you’ll need to do a file by file merge with your forked version. This rarely goes well.

Also, your run the risk of changing expected behavior of methods by having them return different values, not taking actions that the system may depend on, or alter data in unexpected, (rather than expected), ways. We’ll talk more about this below.

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

Including Different Classes

Magento, or more accurately PHP, searches for class files in the following Magento folders.

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

Because of this, and because of the order Magento constructs PHP’s include paths, placing a copy of a core file in the app/code/local folder means PHP will include it first. So if you wanted to change the functionality of the Product Model, you’d add 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 core file, and therefore the core file never needs to be included. This avoids the problem of merging files that hacking the source creates, and also centralizes all your customizations in one directory structure.

However, this is still only marginally better, and a solution you should avoid. Similar to hacking the core system files, you’re risking a changing the behavior of vital class methods.

For example, consider the getName method on the afformentioned Product Model

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

While overriding this method, you might inadvertently add a code path in your override 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 return a string, your customizations might break those other parts of the system. This gets even worse when methods are returning objects, as trying to call a method on null will result in a fatal error (this is part of what Java and C# programmers are harping on about w/r/t type safety in PHP)

Next, consider the validate method in 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 might inadvertently remove the dispatching events.

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

Other parts of the system that rely on these events being fired would stop working.

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

Using the Override/Rewrite System

Magento’s class override/Rewrite system relies on the use of a factory pattern for creating Models, Helpers, and Blocks. When you say

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

you’re telling Magento

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

In turn, Magento then consults its system configuration files and says

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

Magento then instantiates and returns the Model for you.

When you override a class in Magentoo, you’re changing the configuration files to say

“hey, if a “catalog/product” Model is instantiated, use my class (Myp_Mym_Model_Product) instead of the core class

Then, when you define your class, you have it extend 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 avoid the problem of merging files during an upgrade and the problem of your class containing outdated methods after an upgrade.

However, there’s still the matter of changing method behavior. Consider, again, the getName and validate methods. Your new methods could just as easily forget/change-the-type-of a return value, or leave out a critical piece of functionality in the original methods.

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 override/rewrite system won’t protect you from this, but it will give you ways to avoid it. Because we’re actually extending the original class, we can 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’ve ensured that any actions that need to take place will take place. Also, by getting the original return value, you’ve reduced the chances that your method will return something unexpected.

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

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

return parent::originalMethod();

If my overrides need that original method to run first, I used a construct something like

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 points 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 doesn’t prevent someone (including me) from overwriting $original_return with something else, but it does discourage it and makes it obvious when someone has. If there’s a method in my override class that doesn’t end in either $original_return->getValue(); or parent::method my eyes are immediately drawn to it as a possible culprit for whatever problem I’m debugging.

Finally, there are times where you want, (or think you want) to change the return value of a core method. When the need for this arrises, I find it’s much safer to define a new method that calls the original, and then change your theme to call 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 ensures any additional side effects created by the original method still occur, the original return type/result is the same, and you can still add your custom logic to specific parts of the system.

Wrapup

Upgradability to any major, or even minor, version of a software package you don’t control is always going to be a bumpy ride. Apple and Microsoft spend millions of dollars testing their upgrade paths during new OS releases, and the Internet is still filled with horror stories of the edge cases they miss. Even inside organizations where everyone’s working towards the same goal new versions often bring unspoken assumptions to the surface quickly as builds break and developers are forced to acknowledge the reality that they work with other human beings.

As a user of the Magento Ecommerce System, your job is to ensure that changes to the core system are kept to a minimum, and that when those changes are needed they’re done in a clean, easily diagnosable matter. Leveraging the Magento override/rewrite system is a powerful tool towards this end.

Like this article? Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

类别 :  magento(258)  |  浏览(2649)  |  评论(0)
发表评论(评论将通过邮件发给作者):

Email: