bug #31043 [VarExporter] support PHP7.4 __serialize & __unserialize (nicolas-grekas)

This PR was merged into the 4.2 branch.

Discussion
----------

[VarExporter] support PHP7.4 __serialize & __unserialize

| Q             | A
| ------------- | ---
| Branch?       | 4.2
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

This PR adds support for the new `__serialize` and `__unserialiaze` magic methods and the related language semantics [introduced in PHP 7.4](https://wiki.php.net/rfc/custom_object_serialization).

As a reminder, our policy is to consider supporting a new version of PHP a bugfix.

Commits
-------

c7a504c822 [VarExporter] support PHP7.4 __serialize & __unserialize
This commit is contained in:
Fabien Potencier 2019-04-10 08:18:41 +02:00
commit 5859749e05
8 changed files with 100 additions and 14 deletions

View File

@ -67,7 +67,7 @@ final class Instantiator
$wrappedInstance = [$reflector->newInstanceWithoutConstructor()];
} elseif (null === Registry::$prototypes[$class]) {
throw new NotInstantiableTypeException($class);
} elseif ($reflector->implementsInterface('Serializable')) {
} elseif ($reflector->implementsInterface('Serializable') && (\PHP_VERSION_ID < 70400 || !method_exists($class, '__unserialize'))) {
$wrappedInstance = [unserialize('C:'.\strlen($class).':"'.$class.'":0:{}')];
} else {
$wrappedInstance = [unserialize('O:'.\strlen($class).':"'.$class.'":0:{}')];

View File

@ -74,10 +74,23 @@ class Exporter
}
$class = \get_class($value);
$reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class);
if ($reflector->hasMethod('__serialize')) {
if (!$reflector->getMethod('__serialize')->isPublic()) {
throw new \Error(sprintf('Call to %s method %s::__serialize()', $reflector->getMethod('__serialize')->isProtected() ? 'protected' : 'private', $class));
}
if (!\is_array($properties = $value->__serialize())) {
throw new \Typerror($class.'::__serialize() must return an array');
}
goto prepare_value;
}
$properties = [];
$sleep = null;
$arrayValue = (array) $value;
$reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class);
$proto = Registry::$prototypes[$class];
if (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) {
@ -154,10 +167,11 @@ class Exporter
}
}
prepare_value:
$objectsPool[$value] = [$id = \count($objectsPool)];
$properties = self::prepare($properties, $objectsPool, $refsPool, $objectsCount, $valueIsStatic);
++$objectsCount;
$objectsPool[$value] = [$id, $class, $properties, \method_exists($class, '__wakeup') ? $objectsCount : 0];
$objectsPool[$value] = [$id, $class, $properties, \method_exists($class, '__unserialize') ? -$objectsCount : (\method_exists($class, '__wakeup') ? $objectsCount : 0)];
$value = new Reference($id);

View File

@ -42,8 +42,12 @@ class Hydrator
foreach ($properties as $class => $vars) {
(self::$hydrators[$class] ?? self::getHydrator($class))($vars, $objects);
}
foreach ($wakeups as $i) {
$objects[$i]->__wakeup();
foreach ($wakeups as $k => $v) {
if (\is_array($v)) {
$objects[-$k]->__unserialize($v);
} else {
$objects[$v]->__wakeup();
}
}
return $value;

View File

@ -86,14 +86,14 @@ class Registry
$proto = $reflector->newInstanceWithoutConstructor();
$instantiableWithoutConstructor = true;
} catch (\ReflectionException $e) {
$proto = $reflector->implementsInterface('Serializable') ? 'C:' : 'O:';
$proto = $reflector->implementsInterface('Serializable') && (\PHP_VERSION_ID < 70400 || !\method_exists($class, '__unserialize')) ? 'C:' : 'O:';
if ('C:' === $proto && !$reflector->getMethod('unserialize')->isInternal()) {
$proto = null;
} elseif (false === $proto = @unserialize($proto.\strlen($class).':"'.$class.'":0:{}')) {
throw new NotInstantiableTypeException($class);
}
}
if (null !== $proto && !$proto instanceof \Throwable && !$proto instanceof \Serializable && !\method_exists($class, '__sleep')) {
if (null !== $proto && !$proto instanceof \Throwable && !$proto instanceof \Serializable && !\method_exists($class, '__sleep') && (\PHP_VERSION_ID < 70400 || !\method_exists($class, '__serialize'))) {
try {
serialize($proto);
} catch (\Exception $e) {
@ -103,7 +103,7 @@ class Registry
}
if (null === $cloneable) {
if (($proto instanceof \Reflector || $proto instanceof \ReflectionGenerator || $proto instanceof \ReflectionType || $proto instanceof \IteratorIterator || $proto instanceof \RecursiveIteratorIterator) && (!$proto instanceof \Serializable && !\method_exists($proto, '__wakeup'))) {
if (($proto instanceof \Reflector || $proto instanceof \ReflectionGenerator || $proto instanceof \ReflectionType || $proto instanceof \IteratorIterator || $proto instanceof \RecursiveIteratorIterator) && (!$proto instanceof \Serializable && !\method_exists($proto, '__wakeup') && (\PHP_VERSION_ID < 70400 || !\method_exists($class, '__unserialize')))) {
throw new NotInstantiableTypeException($class);
}

View File

@ -3,7 +3,8 @@ VarExporter Component
The VarExporter component allows exporting any serializable PHP data structure to
plain PHP code. While doing so, it preserves all the semantics associated with
the serialization mechanism of PHP (`__wakeup`, `__sleep`, `Serializable`).
the serialization mechanism of PHP (`__wakeup`, `__sleep`, `Serializable`,
`__serialize`, `__unserialize`).
It also provides an instantiator that allows creating and populating objects
without calling their constructor nor any other methods.

View File

@ -0,0 +1,16 @@
<?php
return \Symfony\Component\VarExporter\Internal\Hydrator::hydrate(
$o = [
clone (($p = &\Symfony\Component\VarExporter\Internal\Registry::$prototypes)['Symfony\\Component\\VarExporter\\Tests\\Php74Serializable'] ?? \Symfony\Component\VarExporter\Internal\Registry::p('Symfony\\Component\\VarExporter\\Tests\\Php74Serializable')),
clone ($p['stdClass'] ?? \Symfony\Component\VarExporter\Internal\Registry::p('stdClass')),
],
null,
[],
$o[0],
[
[
$o[1],
],
]
);

View File

@ -82,7 +82,7 @@ class VarExporterTest extends TestCase
$marshalledValue = VarExporter::export($value, $isStaticValue);
$this->assertSame($staticValueExpected, $isStaticValue);
if ('var-on-sleep' !== $testName) {
if ('var-on-sleep' !== $testName && 'php74-serializable' !== $testName) {
$this->assertDumpEquals($dumpedValue, $value);
}
@ -199,6 +199,8 @@ class VarExporterTest extends TestCase
yield ['foo-serializable', new FooSerializable('bar')];
yield ['private-constructor', PrivateConstructor::create('bar')];
yield ['php74-serializable', new Php74Serializable()];
}
}
@ -387,3 +389,36 @@ class FooSerializable implements \Serializable
list($this->foo) = unserialize($str);
}
}
class Php74Serializable implements \Serializable
{
public function __serialize()
{
return [$this->foo = new \stdClass()];
}
public function __unserialize(array $data)
{
list($this->foo) = $data;
}
public function __sleep()
{
throw new \BadMethodCallException();
}
public function __wakeup()
{
throw new \BadMethodCallException();
}
public function serialize()
{
throw new \BadMethodCallException();
}
public function unserialize($ser)
{
throw new \BadMethodCallException();
}
}

View File

@ -69,14 +69,30 @@ final class VarExporter
$classes = [];
$values = [];
$wakeups = [];
$states = [];
foreach ($objectsPool as $i => $v) {
list(, $classes[], $values[], $wakeup) = $objectsPool[$v];
if ($wakeup) {
$wakeups[$wakeup] = $i;
if (0 < $wakeup) {
$states[$wakeup] = $i;
} elseif (0 > $wakeup) {
$states[-$wakeup] = [$i, array_pop($values)];
$values[] = [];
}
}
ksort($wakeups);
ksort($states);
$wakeups = [null];
foreach ($states as $k => $v) {
if (\is_array($v)) {
$wakeups[-$v[0]] = $v[1];
} else {
$wakeups[] = $v;
}
}
if (null === $wakeups[0]) {
unset($wakeups[0]);
}
$properties = [];
foreach ($values as $i => $vars) {