[VarExporter] support PHP7.4 __serialize & __unserialize

This commit is contained in:
Nicolas Grekas 2019-04-09 19:38:59 +02:00
parent 75b1157633
commit c7a504c822
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) {