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:
commit
5859749e05
@ -67,7 +67,7 @@ final class Instantiator
|
|||||||
$wrappedInstance = [$reflector->newInstanceWithoutConstructor()];
|
$wrappedInstance = [$reflector->newInstanceWithoutConstructor()];
|
||||||
} elseif (null === Registry::$prototypes[$class]) {
|
} elseif (null === Registry::$prototypes[$class]) {
|
||||||
throw new NotInstantiableTypeException($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:{}')];
|
$wrappedInstance = [unserialize('C:'.\strlen($class).':"'.$class.'":0:{}')];
|
||||||
} else {
|
} else {
|
||||||
$wrappedInstance = [unserialize('O:'.\strlen($class).':"'.$class.'":0:{}')];
|
$wrappedInstance = [unserialize('O:'.\strlen($class).':"'.$class.'":0:{}')];
|
||||||
|
@ -74,10 +74,23 @@ class Exporter
|
|||||||
}
|
}
|
||||||
|
|
||||||
$class = \get_class($value);
|
$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 = [];
|
$properties = [];
|
||||||
$sleep = null;
|
$sleep = null;
|
||||||
$arrayValue = (array) $value;
|
$arrayValue = (array) $value;
|
||||||
$reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class);
|
|
||||||
$proto = Registry::$prototypes[$class];
|
$proto = Registry::$prototypes[$class];
|
||||||
|
|
||||||
if (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) {
|
if (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) {
|
||||||
@ -154,10 +167,11 @@ class Exporter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepare_value:
|
||||||
$objectsPool[$value] = [$id = \count($objectsPool)];
|
$objectsPool[$value] = [$id = \count($objectsPool)];
|
||||||
$properties = self::prepare($properties, $objectsPool, $refsPool, $objectsCount, $valueIsStatic);
|
$properties = self::prepare($properties, $objectsPool, $refsPool, $objectsCount, $valueIsStatic);
|
||||||
++$objectsCount;
|
++$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);
|
$value = new Reference($id);
|
||||||
|
|
||||||
|
@ -42,8 +42,12 @@ class Hydrator
|
|||||||
foreach ($properties as $class => $vars) {
|
foreach ($properties as $class => $vars) {
|
||||||
(self::$hydrators[$class] ?? self::getHydrator($class))($vars, $objects);
|
(self::$hydrators[$class] ?? self::getHydrator($class))($vars, $objects);
|
||||||
}
|
}
|
||||||
foreach ($wakeups as $i) {
|
foreach ($wakeups as $k => $v) {
|
||||||
$objects[$i]->__wakeup();
|
if (\is_array($v)) {
|
||||||
|
$objects[-$k]->__unserialize($v);
|
||||||
|
} else {
|
||||||
|
$objects[$v]->__wakeup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $value;
|
return $value;
|
||||||
|
@ -86,14 +86,14 @@ class Registry
|
|||||||
$proto = $reflector->newInstanceWithoutConstructor();
|
$proto = $reflector->newInstanceWithoutConstructor();
|
||||||
$instantiableWithoutConstructor = true;
|
$instantiableWithoutConstructor = true;
|
||||||
} catch (\ReflectionException $e) {
|
} 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()) {
|
if ('C:' === $proto && !$reflector->getMethod('unserialize')->isInternal()) {
|
||||||
$proto = null;
|
$proto = null;
|
||||||
} elseif (false === $proto = @unserialize($proto.\strlen($class).':"'.$class.'":0:{}')) {
|
} elseif (false === $proto = @unserialize($proto.\strlen($class).':"'.$class.'":0:{}')) {
|
||||||
throw new NotInstantiableTypeException($class);
|
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 {
|
try {
|
||||||
serialize($proto);
|
serialize($proto);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
@ -103,7 +103,7 @@ class Registry
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (null === $cloneable) {
|
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);
|
throw new NotInstantiableTypeException($class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,8 @@ VarExporter Component
|
|||||||
|
|
||||||
The VarExporter component allows exporting any serializable PHP data structure to
|
The VarExporter component allows exporting any serializable PHP data structure to
|
||||||
plain PHP code. While doing so, it preserves all the semantics associated with
|
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
|
It also provides an instantiator that allows creating and populating objects
|
||||||
without calling their constructor nor any other methods.
|
without calling their constructor nor any other methods.
|
||||||
|
@ -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],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
@ -82,7 +82,7 @@ class VarExporterTest extends TestCase
|
|||||||
$marshalledValue = VarExporter::export($value, $isStaticValue);
|
$marshalledValue = VarExporter::export($value, $isStaticValue);
|
||||||
|
|
||||||
$this->assertSame($staticValueExpected, $isStaticValue);
|
$this->assertSame($staticValueExpected, $isStaticValue);
|
||||||
if ('var-on-sleep' !== $testName) {
|
if ('var-on-sleep' !== $testName && 'php74-serializable' !== $testName) {
|
||||||
$this->assertDumpEquals($dumpedValue, $value);
|
$this->assertDumpEquals($dumpedValue, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,6 +199,8 @@ class VarExporterTest extends TestCase
|
|||||||
yield ['foo-serializable', new FooSerializable('bar')];
|
yield ['foo-serializable', new FooSerializable('bar')];
|
||||||
|
|
||||||
yield ['private-constructor', PrivateConstructor::create('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);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -69,14 +69,30 @@ final class VarExporter
|
|||||||
|
|
||||||
$classes = [];
|
$classes = [];
|
||||||
$values = [];
|
$values = [];
|
||||||
$wakeups = [];
|
$states = [];
|
||||||
foreach ($objectsPool as $i => $v) {
|
foreach ($objectsPool as $i => $v) {
|
||||||
list(, $classes[], $values[], $wakeup) = $objectsPool[$v];
|
list(, $classes[], $values[], $wakeup) = $objectsPool[$v];
|
||||||
if ($wakeup) {
|
if (0 < $wakeup) {
|
||||||
$wakeups[$wakeup] = $i;
|
$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 = [];
|
$properties = [];
|
||||||
foreach ($values as $i => $vars) {
|
foreach ($values as $i => $vars) {
|
||||||
|
Reference in New Issue
Block a user