Magento 2.2 Error – Unable to Unserialize Value

fatal errormagento2.2resource-modelunserialize

After upgrade to the version 2.2 my modules which has serialized values does not work any more.

Here is some info from the Magento 2.2 release notes:

Significant enhancements in platform security and developer
experience. Security improvements include the removal of unserialize
calls and protection of this functionality to increase resilence
against dangerous code execution attacks. We have also continued to
review and improve our protection against Cross-Site Scripting (XSS)
attacks.

In general, we’ve removed serialize/unserialize from most the code to
improve protection against remote code execution attacks. We’ve
enhanced protection of code where use of object serialization or
unserialization was unavoidable. Additionally, we’ve increased our use
of output escaping to protect against cross-site scripting (XSS)
attacks.

Its seems ok, but how I can correctly convert a data when a customer updates a module to the new version? In my case update to the version 2.2 does brokes the site with the error:

Unable to unserialize value.

I've added the correct serializer (Magento\Framework\Serialize\Serializer\Serialize) to the my resource models, but seems it is not correct solution.

In the Magento\Framework\Flag class I seen interesting solution, but seems it's not good enough:

/**
 * Retrieve flag data
 *
 * @return mixed
 */
public function getFlagData()
{
    if ($this->hasFlagData()) {
        $flagData = $this->getData('flag_data');
        try {
            $data = $this->json->unserialize($flagData);
        } catch (\InvalidArgumentException $exception) {
            $data = $this->serialize->unserialize($flagData);
        }
        return $data;
    }
}

where:

  • $this->json instance of Magento\Framework\Serialize\Serializer\Json
  • $this->serialize instance of Magento\Framework\Serialize\Serializer\Serialize

So, the question is: what is the correct solution for this case? Should I write the UpgradeData script that does unserialize the old values and serializes them back using Json (re-save all models)?

PS: I've read this posts

but there is no answer for my question.

Best Answer

The problem is in /vendor/magento/framework/Serialize/Serializer/Json.php there is a function unserialize($string) which gives you a syntax error if the string is already serialized.

There is a workaround - you can check if string is serialized and then use serialize($string). Change unserialize to:

public function unserialize($string)
{
    /* Workaround: serialize first if is serialized */
    if($this->is_serialized($string))
    {
        $string = $this->serialize($string);
    }
    $result = json_decode($string, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
         throw new \InvalidArgumentException('Unable to unserialize value.');

    }
    return $result;
}

and add function to check if string is serialized:

function is_serialized($value, &$result = null)
{
    // Bit of a give away this one
    if (!is_string($value))
    {
        return false;
    }
    // Serialized false, return true. unserialize() returns false on an
    // invalid string or it could return false if the string is serialized
    // false, eliminate that possibility.
    if ($value === 'b:0;')
    {
        $result = false;
        return true;
    }
    $length = strlen($value);
    $end    = '';
    switch ($value[0])
    {
        case 's':
            if ($value[$length - 2] !== '"')
            {
                return false;
            }
        case 'b':
        case 'i':
        case 'd':
            // This looks odd but it is quicker than isset()ing
            $end .= ';';
        case 'a':
        case 'O':
            $end .= '}';
            if ($value[1] !== ':')
            {
                return false;
            }
            switch ($value[2])
            {
                case 0:
                case 1:
                case 2:
                case 3:
                case 4:
                case 5:
                case 6:
                case 7:
                case 8:
                case 9:
                    break;
                default:
                    return false;
            }
        case 'N':
            $end .= ';';
            if ($value[$length - 1] !== $end[0])
            {
                return false;
            }
            break;
        default:
            return false;
    }
    if (($result = @unserialize($value)) === false)
    {
        $result = null;
        return false;
    }
    return true;
}

After save fe. category without problem, You can restore class to default and there wont be such problem in future.