Tag Archives: session

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!