Skip to content

Commit c7a504c

Browse files
[VarExporter] support PHP7.4 __serialize & __unserialize
1 parent 75b1157 commit c7a504c

File tree

8 files changed

+100
-14
lines changed

8 files changed

+100
-14
lines changed

src/Symfony/Component/VarExporter/Instantiator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static function instantiate(string $class, array $properties = [], array
6767
$wrappedInstance = [$reflector->newInstanceWithoutConstructor()];
6868
} elseif (null === Registry::$prototypes[$class]) {
6969
throw new NotInstantiableTypeException($class);
70-
} elseif ($reflector->implementsInterface('Serializable')) {
70+
} elseif ($reflector->implementsInterface('Serializable') && (\PHP_VERSION_ID < 70400 || !method_exists($class, '__unserialize'))) {
7171
$wrappedInstance = [unserialize('C:'.\strlen($class).':"'.$class.'":0:{}')];
7272
} else {
7373
$wrappedInstance = [unserialize('O:'.\strlen($class).':"'.$class.'":0:{}')];

src/Symfony/Component/VarExporter/Internal/Exporter.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,23 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount
7474
}
7575

7676
$class = \get_class($value);
77+
$reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class);
78+
79+
if ($reflector->hasMethod('__serialize')) {
80+
if (!$reflector->getMethod('__serialize')->isPublic()) {
81+
throw new \Error(sprintf('Call to %s method %s::__serialize()', $reflector->getMethod('__serialize')->isProtected() ? 'protected' : 'private', $class));
82+
}
83+
84+
if (!\is_array($properties = $value->__serialize())) {
85+
throw new \Typerror($class.'::__serialize() must return an array');
86+
}
87+
88+
goto prepare_value;
89+
}
90+
7791
$properties = [];
7892
$sleep = null;
7993
$arrayValue = (array) $value;
80-
$reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class);
8194
$proto = Registry::$prototypes[$class];
8295

8396
if (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) {
@@ -154,10 +167,11 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount
154167
}
155168
}
156169

170+
prepare_value:
157171
$objectsPool[$value] = [$id = \count($objectsPool)];
158172
$properties = self::prepare($properties, $objectsPool, $refsPool, $objectsCount, $valueIsStatic);
159173
++$objectsCount;
160-
$objectsPool[$value] = [$id, $class, $properties, \method_exists($class, '__wakeup') ? $objectsCount : 0];
174+
$objectsPool[$value] = [$id, $class, $properties, \method_exists($class, '__unserialize') ? -$objectsCount : (\method_exists($class, '__wakeup') ? $objectsCount : 0)];
161175

162176
$value = new Reference($id);
163177

src/Symfony/Component/VarExporter/Internal/Hydrator.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,12 @@ public static function hydrate($objects, $values, $properties, $value, $wakeups)
4242
foreach ($properties as $class => $vars) {
4343
(self::$hydrators[$class] ?? self::getHydrator($class))($vars, $objects);
4444
}
45-
foreach ($wakeups as $i) {
46-
$objects[$i]->__wakeup();
45+
foreach ($wakeups as $k => $v) {
46+
if (\is_array($v)) {
47+
$objects[-$k]->__unserialize($v);
48+
} else {
49+
$objects[$v]->__wakeup();
50+
}
4751
}
4852

4953
return $value;

src/Symfony/Component/VarExporter/Internal/Registry.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,14 @@ public static function getClassReflector($class, $instantiableWithoutConstructor
8686
$proto = $reflector->newInstanceWithoutConstructor();
8787
$instantiableWithoutConstructor = true;
8888
} catch (\ReflectionException $e) {
89-
$proto = $reflector->implementsInterface('Serializable') ? 'C:' : 'O:';
89+
$proto = $reflector->implementsInterface('Serializable') && (\PHP_VERSION_ID < 70400 || !\method_exists($class, '__unserialize')) ? 'C:' : 'O:';
9090
if ('C:' === $proto && !$reflector->getMethod('unserialize')->isInternal()) {
9191
$proto = null;
9292
} elseif (false === $proto = @unserialize($proto.\strlen($class).':"'.$class.'":0:{}')) {
9393
throw new NotInstantiableTypeException($class);
9494
}
9595
}
96-
if (null !== $proto && !$proto instanceof \Throwable && !$proto instanceof \Serializable && !\method_exists($class, '__sleep')) {
96+
if (null !== $proto && !$proto instanceof \Throwable && !$proto instanceof \Serializable && !\method_exists($class, '__sleep') && (\PHP_VERSION_ID < 70400 || !\method_exists($class, '__serialize'))) {
9797
try {
9898
serialize($proto);
9999
} catch (\Exception $e) {
@@ -103,7 +103,7 @@ public static function getClassReflector($class, $instantiableWithoutConstructor
103103
}
104104

105105
if (null === $cloneable) {
106-
if (($proto instanceof \Reflector || $proto instanceof \ReflectionGenerator || $proto instanceof \ReflectionType || $proto instanceof \IteratorIterator || $proto instanceof \RecursiveIteratorIterator) && (!$proto instanceof \Serializable && !\method_exists($proto, '__wakeup'))) {
106+
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')))) {
107107
throw new NotInstantiableTypeException($class);
108108
}
109109

src/Symfony/Component/VarExporter/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ VarExporter Component
33

44
The VarExporter component allows exporting any serializable PHP data structure to
55
plain PHP code. While doing so, it preserves all the semantics associated with
6-
the serialization mechanism of PHP (`__wakeup`, `__sleep`, `Serializable`).
6+
the serialization mechanism of PHP (`__wakeup`, `__sleep`, `Serializable`,
7+
`__serialize`, `__unserialize`).
78

89
It also provides an instantiator that allows creating and populating objects
910
without calling their constructor nor any other methods.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
return \Symfony\Component\VarExporter\Internal\Hydrator::hydrate(
4+
$o = [
5+
clone (($p = &\Symfony\Component\VarExporter\Internal\Registry::$prototypes)['Symfony\\Component\\VarExporter\\Tests\\Php74Serializable'] ?? \Symfony\Component\VarExporter\Internal\Registry::p('Symfony\\Component\\VarExporter\\Tests\\Php74Serializable')),
6+
clone ($p['stdClass'] ?? \Symfony\Component\VarExporter\Internal\Registry::p('stdClass')),
7+
],
8+
null,
9+
[],
10+
$o[0],
11+
[
12+
[
13+
$o[1],
14+
],
15+
]
16+
);

src/Symfony/Component/VarExporter/Tests/VarExporterTest.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public function testExport(string $testName, $value, bool $staticValueExpected =
8282
$marshalledValue = VarExporter::export($value, $isStaticValue);
8383

8484
$this->assertSame($staticValueExpected, $isStaticValue);
85-
if ('var-on-sleep' !== $testName) {
85+
if ('var-on-sleep' !== $testName && 'php74-serializable' !== $testName) {
8686
$this->assertDumpEquals($dumpedValue, $value);
8787
}
8888

@@ -199,6 +199,8 @@ public function provideExport()
199199
yield ['foo-serializable', new FooSerializable('bar')];
200200

201201
yield ['private-constructor', PrivateConstructor::create('bar')];
202+
203+
yield ['php74-serializable', new Php74Serializable()];
202204
}
203205
}
204206

@@ -387,3 +389,36 @@ public function unserialize($str)
387389
list($this->foo) = unserialize($str);
388390
}
389391
}
392+
393+
class Php74Serializable implements \Serializable
394+
{
395+
public function __serialize()
396+
{
397+
return [$this->foo = new \stdClass()];
398+
}
399+
400+
public function __unserialize(array $data)
401+
{
402+
list($this->foo) = $data;
403+
}
404+
405+
public function __sleep()
406+
{
407+
throw new \BadMethodCallException();
408+
}
409+
410+
public function __wakeup()
411+
{
412+
throw new \BadMethodCallException();
413+
}
414+
415+
public function serialize()
416+
{
417+
throw new \BadMethodCallException();
418+
}
419+
420+
public function unserialize($ser)
421+
{
422+
throw new \BadMethodCallException();
423+
}
424+
}

src/Symfony/Component/VarExporter/VarExporter.php

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,30 @@ public static function export($value, bool &$isStaticValue = null): string
6969

7070
$classes = [];
7171
$values = [];
72-
$wakeups = [];
72+
$states = [];
7373
foreach ($objectsPool as $i => $v) {
7474
list(, $classes[], $values[], $wakeup) = $objectsPool[$v];
75-
if ($wakeup) {
76-
$wakeups[$wakeup] = $i;
75+
if (0 < $wakeup) {
76+
$states[$wakeup] = $i;
77+
} elseif (0 > $wakeup) {
78+
$states[-$wakeup] = [$i, array_pop($values)];
79+
$values[] = [];
7780
}
7881
}
79-
ksort($wakeups);
82+
ksort($states);
83+
84+
$wakeups = [null];
85+
foreach ($states as $k => $v) {
86+
if (\is_array($v)) {
87+
$wakeups[-$v[0]] = $v[1];
88+
} else {
89+
$wakeups[] = $v;
90+
}
91+
}
92+
93+
if (null === $wakeups[0]) {
94+
unset($wakeups[0]);
95+
}
8096

8197
$properties = [];
8298
foreach ($values as $i => $vars) {

0 commit comments

Comments
 (0)