diff --git a/src/Symfony/Component/VarExporter/Instantiator.php b/src/Symfony/Component/VarExporter/Instantiator.php index 7eefc3c2d2..06abbc75a6 100644 --- a/src/Symfony/Component/VarExporter/Instantiator.php +++ b/src/Symfony/Component/VarExporter/Instantiator.php @@ -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:{}')]; diff --git a/src/Symfony/Component/VarExporter/Internal/Exporter.php b/src/Symfony/Component/VarExporter/Internal/Exporter.php index fae9084ca0..74a324691e 100644 --- a/src/Symfony/Component/VarExporter/Internal/Exporter.php +++ b/src/Symfony/Component/VarExporter/Internal/Exporter.php @@ -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); diff --git a/src/Symfony/Component/VarExporter/Internal/Hydrator.php b/src/Symfony/Component/VarExporter/Internal/Hydrator.php index 07721df428..5f64adf96f 100644 --- a/src/Symfony/Component/VarExporter/Internal/Hydrator.php +++ b/src/Symfony/Component/VarExporter/Internal/Hydrator.php @@ -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; diff --git a/src/Symfony/Component/VarExporter/Internal/Registry.php b/src/Symfony/Component/VarExporter/Internal/Registry.php index 31ec4a0d79..b5069dd16a 100644 --- a/src/Symfony/Component/VarExporter/Internal/Registry.php +++ b/src/Symfony/Component/VarExporter/Internal/Registry.php @@ -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); } diff --git a/src/Symfony/Component/VarExporter/README.md b/src/Symfony/Component/VarExporter/README.md index c3a072127e..180554ed1a 100644 --- a/src/Symfony/Component/VarExporter/README.md +++ b/src/Symfony/Component/VarExporter/README.md @@ -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. diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/php74-serializable.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/php74-serializable.php new file mode 100644 index 0000000000..06cfac10f5 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/php74-serializable.php @@ -0,0 +1,16 @@ +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(); + } +} diff --git a/src/Symfony/Component/VarExporter/VarExporter.php b/src/Symfony/Component/VarExporter/VarExporter.php index 45ef7446b1..da9a8d4373 100644 --- a/src/Symfony/Component/VarExporter/VarExporter.php +++ b/src/Symfony/Component/VarExporter/VarExporter.php @@ -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) {