Category Archives: Uncategorized

Why model overriding does not work

I just met a case when overriding a module in Magento 1.x didn’t work. After some digging it has turned out that it will work only when the slashed notation of class naming is used. I won’t write down the steps to create a new extension, how to write a class or where to put files, because this article is about how to correctly override a model.

Example, when in the code it is used:

$validator = Mage:getModel("core/url_validator");

then you will be able to override it from your own module, by defining in config.xml:

<global>
    <modules>
        <core>
            <rewrite>
                <url_validator>Foo_Model_Bar_Checker</url_validator>
            </rewrite>
        </core>
    </modules>
</global>

However, if the code would use:

$validator = Mage::getModel("Mage_Core_Model_Url_Validator");

then it would be useless your efforts to override it. Why? The answer is quite obvious. Just take a look at Mage_Core_Model_Config:

public function getModelClassName($modelClass)
{
    $modelClass = trim($modelClass);
    if (strpos($modelClass, '/')===false) {
        return $modelClass;
    }
    return $this->getGroupedClassName('model', $modelClass);
}

When you are using the full class name notation, then it will just return it back. But when using Mage::getModel with slashes in the argument, then it will start parsing the config.xml files after possible rewrites.

It is up to you how you want to declare your models, maybe you want to make harder for others to override your code, then you can always specify the full class name. Or maybe you want your code to be faster by skipping a lot of config.xml parsing, that is also good. Otherwise just use the slashed notation.

Making work Magento with PHP 7 RC1, RC2 and RC3

I was curious whether it will work with PHP 7 the latest version of Magento Community Edition. When I write this article the latest released version is 1.9.2.1. As I expected, Magento has crashed with an ugly error message like:

Fatal error: Uncaught Error: Function name must be a string in ... app\code\core\Mage\Core\Model\Layout.php:555 ...

This error was easy to fix because the problem was in the following line:

$out .= $this->getBlock($callback[0])->$callback[1]();

Instead it should be:

$out .= $this->getBlock($callback[0])->{$callback[1]}();

Since it is not recommended to edit the core files, we will override them, which means that we will create the very similar structure of the core files in app/code/local. Example if we want to override app/code/core/Mage/Core/Model/Layout.php, then we will copy this file into app/code/local/Mage/Core/Model/Layout.php. Magento will automatically include what is in the app/code/local folder. Despite this solution works, this can be considered only a temporary solution until a fixed Magento / PHP 7 will be released. Overriding core files could be dangerous, problems could occur especially after upgrading Magento to a newer version.

This small change seemed to fix Magento, but I was wrong. While the frontend worked well, the backend did not log me in. In the meantime I had a lot of problems getting PHP and Apache configuration files ready. So I didn’t know whether my configuration is bad or simply the new PHP 7 RC1 does not like Magento.
Finally I found out the main reason why the login doesn’t work: despite the authentication of the backend user has happened and I was redirected back to the admin index, the user object was not saved into the session. Investigation was very difficult because currently there is no Xdebug for the unreleased PHP7. After another couple of hours of digging I’ve found out that in one of Magento’s abstract classes it was specified something like:

$this->_data = &$_SESSION;

So Magento just sets $this->_data as a reference to the $_SESSION. Hmm, maybe that thing does not work… And yes. First I just tried to use in the admin/session class instead of

$this->setUser($user);

this:

$_SESSION['admin']['user'] = $user;

then suddenly Magento logged me in. The next step was to make the session related functionality work all over Magento. For this I had to override Mage_Core_Model_Session_Abstract_Varien and had to change getData from:

public function getData($key='', $clear = false)
{
    $data = parent::getData($key);
    if ($clear && isset($this->_data[$key])) {
        unset($this->_data[$key]);
    }
    return $data;
}

to

public function getData($key='', $clear = false)
{
    $data = $this->getSessionData($key);
    if ($clear && isset($_SESSION[$key])) {
        unset($_SESSION[$key]);
    }
    return $data;
}
public function getSessionData($key='', $index=null)
{
    if (''===$key) {
        return $_SESSION;
    }

    $default = null;

    // accept a/b/c as ['a']['b']['c']
    if (strpos($key,'/')) {
        $keyArr = explode('/', $key);
        $data = $_SESSION;
        foreach ($keyArr as $i=>$k) {
            if ($k==='') {
                return $default;
            }
            if (is_array($data)) {
                if (!isset($data[$k])) {
                    return $default;
                }
                $data = $data[$k];
            } elseif ($data instanceof Varien_Object) {
                $data = $data->getData($k);
            } else {
                return $default;
            }
        }
        return $data;
    }

    // legacy functionality for $index
    if (isset($_SESSION[$key])) {
        if (is_null($index)) {
            return $_SESSION[$key];
        }

        $value = $_SESSION[$key];
        if (is_array($value)) {
            //if (isset($value[$index]) && (!empty($value[$index]) || strlen($value[$index]) > 0)) {
            /**
            * If we have any data, even if it empty - we should use it, anyway
            */
            if (isset($value[$index])) {
                return $value[$index];
            }
            return null;
        } elseif (is_string($value)) {
            $arr = explode("\n", $value);
            return (isset($arr[$index]) && (!empty($arr[$index]) || strlen($arr[$index]) > 0)) ? $arr[$index] : null;
        } elseif ($value instanceof Varien_Object) {
            return $value->getData($index);
        }
        return $default;
    }
    return $default;
}

then I had to create the __call magic method to override the parent class’ behavior:

public function __call($method, $args)
{
    if (substr($method, 0, 3) == "has")
    {
        $key = $this->_underscore(substr($method,3));
        return isset($_SESSION[$key]);
    }
    return parent::__call($method, $args);
}

and then I’ve added the modified setData, unsetData and _addFullNames methods:

public function setData($key, $value=null)
{
    $this->_hasDataChanges = true;
    if(is_array($key)) {
        $_SESSION = $key;
        $this->_addFullNames();
    } else {
        $_SESSION[$key] = $value;
        if (isset($this->_syncFieldsMap[$key])) {
            $fullFieldName = $this->_syncFieldsMap[$key];
            $_SESSION[$fullFieldName] = $value;
        }
    }
    return $this;
}
public function unsetData($key=null)
{
    $this->_hasDataChanges = true;
    if (is_null($key)) {
        $_SESSION = array();
    } else {
        unset($_SESSION[$key]);
        if (isset($this->_syncFieldsMap[$key])) {
            $fullFieldName = $this->_syncFieldsMap[$key];
            unset($_SESSION[$fullFieldName]);
        }
    }
    return $this;
}

protected function _addFullNames()
{
    $existedShortKeys = array_intersect($this->_syncFieldsMap, array_keys($_SESSION));
    if (!empty($existedShortKeys)) {
        foreach ($existedShortKeys as $key) {
            $fullFieldName = array_search($key, $this->_syncFieldsMap);
            $_SESSION[$fullFieldName] = $_SESSION[$key];
        }
    }
}

It has worked for me. Maybe it can be fixed with some php.ini setting also, but I really don’t see a reason to disable passing variables by reference. We will find out shortly if PHP 7 RC2 solves this problem.

So, to sum it up:
– the problem with the “fatal error. function name must be a string” can be fixed by overriding Mage_Core_Model_Layout
– the other problem:  getData / setData / unsetData methods does not write into the session, which causes the admin login problem. It can be fixed by overriding Mage_Core_Model_Session_Abstract_Varien.

Happy patching!

p.s. In the meantime the PHP team released the PHP 7 RC2, which has the same behavior as the RC1. It seems that the problem is already reported and it is under discussion by the PHP team.

p.s.2 In PHP 7 RC3 the session-related problem has been solved, so the only thing you need is to fix Layout.php.

p.s.3 If you override Varien.php with this fix, while using the final version of PHP 7, you will meet some serious problems in the frontend, the customers not being able to log in. So I repeat: don’t use it with the final releases of PHP 7!