From 79e2464a12cc5c5b8b0eecf216904574eda6ee8b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 28 Feb 2025 15:26:54 +0100 Subject: [PATCH] [VarExporter] Fix support for abstract properties --- .../VarExporter/Internal/Hydrator.php | 2 +- .../Component/VarExporter/ProxyHelper.php | 40 +++++++++++++----- .../Fixtures/LazyProxy/AbstractHooked.php | 22 ++++++++++ .../Tests/Fixtures/{ => LazyProxy}/Hooked.php | 2 +- .../Fixtures/LazyProxy/HookedInterface.php | 17 ++++++++ .../VarExporter/Tests/LazyGhostTraitTest.php | 2 +- .../VarExporter/Tests/LazyProxyTraitTest.php | 42 ++++++++++++++++++- .../VarExporter/Tests/ProxyHelperTest.php | 2 +- 8 files changed, 113 insertions(+), 16 deletions(-) create mode 100644 src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AbstractHooked.php rename src/Symfony/Component/VarExporter/Tests/Fixtures/{ => LazyProxy}/Hooked.php (88%) create mode 100644 src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/HookedInterface.php diff --git a/src/Symfony/Component/VarExporter/Internal/Hydrator.php b/src/Symfony/Component/VarExporter/Internal/Hydrator.php index 97ffe4c831627..30636ab94a7ab 100644 --- a/src/Symfony/Component/VarExporter/Internal/Hydrator.php +++ b/src/Symfony/Component/VarExporter/Internal/Hydrator.php @@ -288,7 +288,7 @@ 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; + $propertyScopes[$name][4] = true; } } diff --git a/src/Symfony/Component/VarExporter/ProxyHelper.php b/src/Symfony/Component/VarExporter/ProxyHelper.php index 246dc4d404bc7..264c1af29e6ca 100644 --- a/src/Symfony/Component/VarExporter/ProxyHelper.php +++ b/src/Symfony/Component/VarExporter/ProxyHelper.php @@ -33,8 +33,8 @@ public static function generateLazyGhost(\ReflectionClass $class): string if ($class->isFinal()) { throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is final.', $class->name)); } - if ($class->isInterface() || $class->isAbstract()) { - throw new LogicException(sprintf('Cannot generate lazy ghost: "%s" is not a concrete class.', $class->name)); + if ($class->isInterface() || $class->isAbstract() || $class->isTrait()) { + throw new LogicException(\sprintf('Cannot generate lazy ghost: "%s" is not a concrete class.', $class->name)); } if (\stdClass::class !== $class->name && $class->isInternal()) { throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is internal.', $class->name)); @@ -66,6 +66,10 @@ public static function generateLazyGhost(\ReflectionClass $class): string continue; } + if ($p->isFinal()) { + throw new LogicException(sprintf('Cannot generate lazy ghost: property "%s::$%s" is final.', $class->name, $p->name)); + } + $type = self::exportType($p); $hooks .= "\n public {$type} \${$name} {\n"; @@ -89,7 +93,7 @@ public static function generateLazyGhost(\ReflectionClass $class): string $hooks .= " }\n"; } - $propertyScopes = self::exportPropertyScopes($class->name); + $propertyScopes = self::exportPropertyScopes($class->name, $propertyScopes); return <<name} implements \Symfony\Component\VarExporter\LazyObjectInterface @@ -126,13 +130,22 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf throw new LogicException(sprintf('Cannot generate lazy proxy with PHP < 8.3: class "%s" is readonly.', $class->name)); } + $propertyScopes = $class ? Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name) : []; + $abstractProperties = []; $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()]; + if (!isset($scope[4]) || ($p = $scope[3])->isVirtual()) { + $abstractProperties[$name] = isset($scope[4]) && $p->isAbstract() ? $p : false; + continue; } + + if ($p->isFinal()) { + throw new LogicException(\sprintf('Cannot generate lazy proxy: property "%s::$%s" is final.', $class->name, $p->name)); + } + + $abstractProperties[$name] = false; + $hookedProperties[$name] = [$p, $p->getHooks()]; } } @@ -143,8 +156,9 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf } $methodReflectors[] = $interface->getMethods(); - if (\PHP_VERSION_ID >= 80400 && !$class) { + if (\PHP_VERSION_ID >= 80400) { foreach ($interface->getProperties() as $p) { + $abstractProperties[$p->name] ??= $p; $hookedProperties[$p->name] ??= [$p, []]; $hookedProperties[$p->name][1] += $p->getHooks(); } @@ -152,6 +166,13 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf } $hooks = ''; + + foreach (array_filter($abstractProperties) as $name => $p) { + $type = self::exportType($p); + $hooks .= "\n public {$type} \${$name};\n"; + unset($propertyScopes[$name][4]); + } + foreach ($hookedProperties as $name => [$p, $methods]) { $type = self::exportType($p); $hooks .= "\n public {$type} \${$p->name} {\n"; @@ -287,7 +308,7 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf $methods = ['initializeLazyObject' => implode('', $body).' }'] + $methods; } $body = $methods ? "\n".implode("\n\n", $methods)."\n" : ''; - $propertyScopes = $class ? self::exportPropertyScopes($class->name) : '[]'; + $propertyScopes = $class ? self::exportPropertyScopes($class->name, $propertyScopes) : '[]'; if ( $class?->hasMethod('__unserialize') @@ -444,9 +465,8 @@ public static function exportType(\ReflectionFunctionAbstract|\ReflectionPropert return implode($glue, $types); } - private static function exportPropertyScopes(string $parent): string + private static function exportPropertyScopes(string $parent, array $propertyScopes): string { - $propertyScopes = Hydrator::$propertyScopes[$parent] ??= Hydrator::getPropertyScopes($parent); uksort($propertyScopes, 'strnatcmp'); foreach ($propertyScopes as $k => $v) { unset($propertyScopes[$k][3]); diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AbstractHooked.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AbstractHooked.php new file mode 100644 index 0000000000000..3d9cde20c7b15 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/AbstractHooked.php @@ -0,0 +1,22 @@ + + * + * 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\LazyProxy; + +abstract class AbstractHooked implements HookedInterface +{ + abstract public string $bar { get; } + + public int $backed { + get { return $this->backed ??= 234; } + set { $this->backed = $value; } + } +} diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/Hooked.php similarity index 88% rename from src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php rename to src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/Hooked.php index 0c46d37afe922..62174f92d5847 100644 --- a/src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/Hooked.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\VarExporter\Tests\Fixtures; +namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy; class Hooked { diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/HookedInterface.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/HookedInterface.php new file mode 100644 index 0000000000000..9cdafd9c1fdfa --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/HookedInterface.php @@ -0,0 +1,17 @@ + + * + * 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\LazyProxy; + +interface HookedInterface +{ + public string $foo { get; } +} diff --git a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php index 00f090a43c292..8351ec0c79a93 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php @@ -17,7 +17,6 @@ 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; @@ -26,6 +25,7 @@ use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\MagicClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ReadOnlyClass; use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\TestClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\SimpleObject; class LazyGhostTraitTest extends TestCase diff --git a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php index 938b304461291..4f0702fd97452 100644 --- a/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php +++ b/src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php @@ -18,7 +18,9 @@ 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\LazyGhost\RegularClass; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\AbstractHooked; +use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\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; @@ -302,11 +304,12 @@ public function testNormalization() /** * @requires PHP 8.4 */ - public function testPropertyHooks() + public function testConcretePropertyHooks() { $initialized = false; $object = $this->createLazyProxy(Hooked::class, function () use (&$initialized) { $initialized = true; + return new Hooked(); }); @@ -318,6 +321,7 @@ public function testPropertyHooks() $initialized = false; $object = $this->createLazyProxy(Hooked::class, function () use (&$initialized) { $initialized = true; + return new Hooked(); }); @@ -326,6 +330,40 @@ public function testPropertyHooks() $this->assertSame(345, $object->backed); } + /** + * @requires PHP 8.4 + */ + public function testAbstractPropertyHooks() + { + $initialized = false; + $object = $this->createLazyProxy(AbstractHooked::class, function () use (&$initialized) { + $initialized = true; + + return new class extends AbstractHooked { + public string $foo = 'Foo'; + public string $bar = 'Bar'; + }; + }); + + $this->assertSame('Foo', $object->foo); + $this->assertSame('Bar', $object->bar); + $this->assertTrue($initialized); + + $initialized = false; + $object = $this->createLazyProxy(AbstractHooked::class, function () use (&$initialized) { + $initialized = true; + + return new class extends AbstractHooked { + public string $foo = 'Foo'; + public string $bar = 'Bar'; + }; + }); + + $this->assertSame('Bar', $object->bar); + $this->assertSame('Foo', $object->foo); + $this->assertTrue($initialized); + } + /** * @template T * diff --git a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php index d0085a70498c5..a8dcc21084c66 100644 --- a/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php +++ b/src/Symfony/Component/VarExporter/Tests/ProxyHelperTest.php @@ -14,7 +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\Hooked; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Php82NullStandaloneReturnType; use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\StringMagicGetClass;