From 540253a89b7a90130f683091558a8b167e425e13 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 12 Feb 2025 17:44:25 +0100 Subject: [PATCH] [VarExporter] Fix lazy objects with hooked properties --- .../VarExporter/Internal/Hydrator.php | 2 + .../Internal/LazyObjectRegistry.php | 17 ++- .../Component/VarExporter/ProxyHelper.php | 109 +++++++++++++++++- .../VarExporter/Tests/Fixtures/Hooked.php | 25 ++++ .../VarExporter/Tests/LazyGhostTraitTest.php | 26 +++++ .../VarExporter/Tests/LazyProxyTraitTest.php | 28 +++++ .../VarExporter/Tests/ProxyHelperTest.php | 12 ++ 7 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php diff --git a/src/Symfony/Component/VarExporter/Internal/Hydrator.php b/src/Symfony/Component/VarExporter/Internal/Hydrator.php index 49d636fb8e0ce..97ffe4c831627 100644 --- a/src/Symfony/Component/VarExporter/Internal/Hydrator.php +++ b/src/Symfony/Component/VarExporter/Internal/Hydrator.php @@ -287,6 +287,8 @@ public static function getPropertyScopes($class) if (\ReflectionProperty::IS_PROTECTED & $flags) { $propertyScopes["\0*\0$name"] = $propertyScopes[$name]; + } elseif (\PHP_VERSION_ID >= 80400 && $property->getHooks()) { + $propertyScopes[$name][] = true; } } diff --git a/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php index fddc6fb3b9664..a7b4987e3b0db 100644 --- a/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php +++ b/src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php @@ -50,6 +50,7 @@ class LazyObjectRegistry public static function getClassResetters($class) { $classProperties = []; + $hookedProperties = []; if ((self::$classReflectors[$class] ??= new \ReflectionClass($class))->isInternal()) { $propertyScopes = []; @@ -60,7 +61,13 @@ public static function getClassResetters($class) foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) { $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name; - if ($k === $key && "\0$class\0lazyObjectState" !== $k) { + if ($k !== $key || "\0$class\0lazyObjectState" === $k) { + continue; + } + + if ($k === $name && ($propertyScopes[$k][4] ?? false)) { + $hookedProperties[$k] = true; + } else { $classProperties[$readonlyScope ?? $scope][$name] = $key; } } @@ -76,9 +83,13 @@ public static function getClassResetters($class) }, null, $scope); } - $resetters[] = static function ($instance, $skippedProperties, $onlyProperties = null) { + $resetters[] = static function ($instance, $skippedProperties, $onlyProperties = null) use ($hookedProperties) { foreach ((array) $instance as $name => $value) { - if ("\0" !== ($name[0] ?? '') && !\array_key_exists($name, $skippedProperties) && (null === $onlyProperties || \array_key_exists($name, $onlyProperties))) { + if ("\0" !== ($name[0] ?? '') + && !\array_key_exists($name, $skippedProperties) + && (null === $onlyProperties || \array_key_exists($name, $onlyProperties)) + && !isset($hookedProperties[$name]) + ) { unset($instance->$name); } } diff --git a/src/Symfony/Component/VarExporter/ProxyHelper.php b/src/Symfony/Component/VarExporter/ProxyHelper.php index d5a8d7418b807..246dc4d404bc7 100644 --- a/src/Symfony/Component/VarExporter/ProxyHelper.php +++ b/src/Symfony/Component/VarExporter/ProxyHelper.php @@ -58,6 +58,37 @@ public static function generateLazyGhost(\ReflectionClass $class): string throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" extends "%s" which is internal.', $class->name, $parent->name)); } } + + $hooks = ''; + $propertyScopes = Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name); + foreach ($propertyScopes as $name => $scope) { + if (!isset($scope[4]) || ($p = $scope[3])->isVirtual()) { + continue; + } + + $type = self::exportType($p); + $hooks .= "\n public {$type} \${$name} {\n"; + + foreach ($p->getHooks() as $hook => $method) { + if ($method->isFinal()) { + throw new LogicException(sprintf('Cannot generate lazy ghost: hook "%s::%s()" is final.', $class->name, $method->name)); + } + + if ('get' === $hook) { + $ref = ($method->returnsReference() ? '&' : ''); + $hooks .= " {$ref}get { \$this->initializeLazyObject(); return parent::\${$name}::get(); }\n"; + } elseif ('set' === $hook) { + $parameters = self::exportParameters($method, true); + $arg = '$'.$method->getParameters()[0]->name; + $hooks .= " set({$parameters}) { \$this->initializeLazyObject(); parent::\${$name}::set({$arg}); }\n"; + } else { + throw new LogicException(sprintf('Cannot generate lazy ghost: hook "%s::%s()" is not supported.', $class->name, $method->name)); + } + } + + $hooks .= " }\n"; + } + $propertyScopes = self::exportPropertyScopes($class->name); return <<name)); } + $hookedProperties = []; + if (\PHP_VERSION_ID >= 80400 && $class) { + $propertyScopes = Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name); + foreach ($propertyScopes as $name => $scope) { + if (isset($scope[4]) && !($p = $scope[3])->isVirtual()) { + $hookedProperties[$name] = [$p, $p->getHooks()]; + } + } + } + $methodReflectors = [$class?->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) ?? []]; foreach ($interfaces as $interface) { if (!$interface->isInterface()) { throw new LogicException(sprintf('Cannot generate lazy proxy: "%s" is not an interface.', $interface->name)); } $methodReflectors[] = $interface->getMethods(); + + if (\PHP_VERSION_ID >= 80400 && !$class) { + foreach ($interface->getProperties() as $p) { + $hookedProperties[$p->name] ??= [$p, []]; + $hookedProperties[$p->name][1] += $p->getHooks(); + } + } + } + + $hooks = ''; + foreach ($hookedProperties as $name => [$p, $methods]) { + $type = self::exportType($p); + $hooks .= "\n public {$type} \${$p->name} {\n"; + + foreach ($methods as $hook => $method) { + if ($method->isFinal()) { + throw new LogicException(sprintf('Cannot generate lazy proxy: hook "%s::%s()" is final.', $class->name, $method->name)); + } + + if ('get' === $hook) { + $ref = ($method->returnsReference() ? '&' : ''); + $hooks .= <<lazyObjectState)) { + return (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$p->name}; + } + + return parent::\${$p->name}::get(); + } + + EOPHP; + } elseif ('set' === $hook) { + $parameters = self::exportParameters($method, true); + $arg = '$'.$method->getParameters()[0]->name; + $hooks .= <<lazyObjectState)) { + \$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)(); + \$this->lazyObjectState->realInstance->{$p->name} = {$arg}; + } + + parent::\${$p->name}::set({$arg}); + } + + EOPHP; + } else { + throw new LogicException(sprintf('Cannot generate lazy proxy: hook "%s::%s()" is not supported.', $class->name, $method->name)); + } + } + + $hooks .= " }\n"; } - $methodReflectors = array_merge(...$methodReflectors); $extendsInternalClass = false; if ($parent = $class) { @@ -112,6 +203,7 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf } $methodsHaveToBeProxied = $extendsInternalClass; $methods = []; + $methodReflectors = array_merge(...$methodReflectors); foreach ($methodReflectors as $method) { if ('__get' !== strtolower($method->name) || 'mixed' === ($type = self::exportType($method) ?? 'mixed')) { @@ -228,7 +320,7 @@ public function __unserialize(\$data): void {$lazyProxyTraitStatement} private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes}; - {$body}} + {$hooks}{$body}} // Help opcache.preload discover always-needed symbols class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); @@ -238,7 +330,7 @@ class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); EOPHP; } - public static function exportSignature(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string + public static function exportParameters(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string { $byRefIndex = 0; $args = ''; @@ -268,8 +360,15 @@ public static function exportSignature(\ReflectionFunctionAbstract $function, bo $args = implode(', ', $args); } + return implode(', ', $parameters); + } + + public static function exportSignature(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string + { + $parameters = self::exportParameters($function, $withParameterTypes, $args); + $signature = 'function '.($function->returnsReference() ? '&' : '') - .($function->isClosure() ? '' : $function->name).'('.implode(', ', $parameters).')'; + .($function->isClosure() ? '' : $function->name).'('.$parameters.')'; if ($function instanceof \ReflectionMethod) { $signature = ($function->isPublic() ? 'public ' : ($function->isProtected() ? 'protected ' : 'private ')) diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php new file mode 100644 index 0000000000000..0c46d37afe922 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Tests\Fixtures; + +class Hooked +{ + public int $notBacked { + get { return 123; } + set { throw \LogicException('Cannot set value.'); } + } + + public int $backed { + get { return $this->backed ??= 234; } + set { $this->backed = $value; } + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php index 68e76a7dac1fa..00f090a43c292 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\VarExporter\Internal\LazyObjectState; use Symfony\Component\VarExporter\ProxyHelper; +use Symfony\Component\VarExporter\Tests\Fixtures\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildMagicClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildStdClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildTestClass; @@ -478,6 +479,31 @@ public function testNormalization() $this->assertSame(['property' => 'property', 'method' => 'method'], $output); } + /** + * @requires PHP 8.4 + */ + public function testPropertyHooks() + { + $initialized = false; + $object = $this->createLazyGhost(Hooked::class, function ($instance) use (&$initialized) { + $initialized = true; + }); + + $this->assertSame(123, $object->notBacked); + $this->assertFalse($initialized); + $this->assertSame(234, $object->backed); + $this->assertTrue($initialized); + + $initialized = false; + $object = $this->createLazyGhost(Hooked::class, function ($instance) use (&$initialized) { + $initialized = true; + }); + + $object->backed = 345; + $this->assertTrue($initialized); + $this->assertSame(345, $object->backed); + } + /** * @template T * diff --git a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php index c4234d085b6dc..938b304461291 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php @@ -18,6 +18,7 @@ use Symfony\Component\VarExporter\Exception\LogicException; use Symfony\Component\VarExporter\LazyProxyTrait; use Symfony\Component\VarExporter\ProxyHelper; +use Symfony\Component\VarExporter\Tests\Fixtures\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\FinalPublicClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\ReadOnlyClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\StringMagicGetClass; @@ -298,6 +299,33 @@ public function testNormalization() $this->assertSame(['property' => 'property', 'method' => 'method'], $output); } + /** + * @requires PHP 8.4 + */ + public function testPropertyHooks() + { + $initialized = false; + $object = $this->createLazyProxy(Hooked::class, function () use (&$initialized) { + $initialized = true; + return new Hooked(); + }); + + $this->assertSame(123, $object->notBacked); + $this->assertFalse($initialized); + $this->assertSame(234, $object->backed); + $this->assertTrue($initialized); + + $initialized = false; + $object = $this->createLazyProxy(Hooked::class, function () use (&$initialized) { + $initialized = true; + return new Hooked(); + }); + + $object->backed = 345; + $this->assertTrue($initialized); + $this->assertSame(345, $object->backed); + } + /** * @template T * diff --git a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php index b7372632de217..d0085a70498c5 100644 --- a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php +++ b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\VarExporter\Exception\LogicException; use Symfony\Component\VarExporter\ProxyHelper; +use Symfony\Component\VarExporter\Tests\Fixtures\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Php82NullStandaloneReturnType; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\StringMagicGetClass; @@ -246,6 +247,17 @@ public function testNullStandaloneReturnType() ProxyHelper::generateLazyProxy(new \ReflectionClass(Php82NullStandaloneReturnType::class)) ); } + + /** + * @requires PHP 8.4 + */ + public function testPropertyHooks() + { + self::assertStringContainsString( + "[parent::class, 'backed', null, 4 => true]", + ProxyHelper::generateLazyProxy(new \ReflectionClass(Hooked::class)) + ); + } } abstract class TestForProxyHelper pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy