Skip to content

Commit 5d6c29a

Browse files
bug symfony#59881 [VarExporter] Fix support for abstract properties (nicolas-grekas)
This PR was merged into the 6.4 branch. Discussion ---------- [VarExporter] Fix support for abstract properties | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | - | License | MIT Commits ------- 79e2464 [VarExporter] Fix support for abstract properties
2 parents 0266d3c + 79e2464 commit 5d6c29a

File tree

8 files changed

+113
-16
lines changed

8 files changed

+113
-16
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ public static function getPropertyScopes($class)
288288
if (\ReflectionProperty::IS_PROTECTED & $flags) {
289289
$propertyScopes["\0*\0$name"] = $propertyScopes[$name];
290290
} elseif (\PHP_VERSION_ID >= 80400 && $property->getHooks()) {
291-
$propertyScopes[$name][] = true;
291+
$propertyScopes[$name][4] = true;
292292
}
293293
}
294294

src/Symfony/Component/VarExporter/ProxyHelper.php

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ public static function generateLazyGhost(\ReflectionClass $class): string
3333
if ($class->isFinal()) {
3434
throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is final.', $class->name));
3535
}
36-
if ($class->isInterface() || $class->isAbstract()) {
37-
throw new LogicException(sprintf('Cannot generate lazy ghost: "%s" is not a concrete class.', $class->name));
36+
if ($class->isInterface() || $class->isAbstract() || $class->isTrait()) {
37+
throw new LogicException(\sprintf('Cannot generate lazy ghost: "%s" is not a concrete class.', $class->name));
3838
}
3939
if (\stdClass::class !== $class->name && $class->isInternal()) {
4040
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
6666
continue;
6767
}
6868

69+
if ($p->isFinal()) {
70+
throw new LogicException(sprintf('Cannot generate lazy ghost: property "%s::$%s" is final.', $class->name, $p->name));
71+
}
72+
6973
$type = self::exportType($p);
7074
$hooks .= "\n public {$type} \${$name} {\n";
7175

@@ -89,7 +93,7 @@ public static function generateLazyGhost(\ReflectionClass $class): string
8993
$hooks .= " }\n";
9094
}
9195

92-
$propertyScopes = self::exportPropertyScopes($class->name);
96+
$propertyScopes = self::exportPropertyScopes($class->name, $propertyScopes);
9397

9498
return <<<EOPHP
9599
extends \\{$class->name} implements \Symfony\Component\VarExporter\LazyObjectInterface
@@ -126,13 +130,22 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf
126130
throw new LogicException(sprintf('Cannot generate lazy proxy with PHP < 8.3: class "%s" is readonly.', $class->name));
127131
}
128132

133+
$propertyScopes = $class ? Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name) : [];
134+
$abstractProperties = [];
129135
$hookedProperties = [];
130136
if (\PHP_VERSION_ID >= 80400 && $class) {
131-
$propertyScopes = Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name);
132137
foreach ($propertyScopes as $name => $scope) {
133-
if (isset($scope[4]) && !($p = $scope[3])->isVirtual()) {
134-
$hookedProperties[$name] = [$p, $p->getHooks()];
138+
if (!isset($scope[4]) || ($p = $scope[3])->isVirtual()) {
139+
$abstractProperties[$name] = isset($scope[4]) && $p->isAbstract() ? $p : false;
140+
continue;
135141
}
142+
143+
if ($p->isFinal()) {
144+
throw new LogicException(\sprintf('Cannot generate lazy proxy: property "%s::$%s" is final.', $class->name, $p->name));
145+
}
146+
147+
$abstractProperties[$name] = false;
148+
$hookedProperties[$name] = [$p, $p->getHooks()];
136149
}
137150
}
138151

@@ -143,15 +156,23 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf
143156
}
144157
$methodReflectors[] = $interface->getMethods();
145158

146-
if (\PHP_VERSION_ID >= 80400 && !$class) {
159+
if (\PHP_VERSION_ID >= 80400) {
147160
foreach ($interface->getProperties() as $p) {
161+
$abstractProperties[$p->name] ??= $p;
148162
$hookedProperties[$p->name] ??= [$p, []];
149163
$hookedProperties[$p->name][1] += $p->getHooks();
150164
}
151165
}
152166
}
153167

154168
$hooks = '';
169+
170+
foreach (array_filter($abstractProperties) as $name => $p) {
171+
$type = self::exportType($p);
172+
$hooks .= "\n public {$type} \${$name};\n";
173+
unset($propertyScopes[$name][4]);
174+
}
175+
155176
foreach ($hookedProperties as $name => [$p, $methods]) {
156177
$type = self::exportType($p);
157178
$hooks .= "\n public {$type} \${$p->name} {\n";
@@ -287,7 +308,7 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf
287308
$methods = ['initializeLazyObject' => implode('', $body).' }'] + $methods;
288309
}
289310
$body = $methods ? "\n".implode("\n\n", $methods)."\n" : '';
290-
$propertyScopes = $class ? self::exportPropertyScopes($class->name) : '[]';
311+
$propertyScopes = $class ? self::exportPropertyScopes($class->name, $propertyScopes) : '[]';
291312

292313
if (
293314
$class?->hasMethod('__unserialize')
@@ -444,9 +465,8 @@ public static function exportType(\ReflectionFunctionAbstract|\ReflectionPropert
444465
return implode($glue, $types);
445466
}
446467

447-
private static function exportPropertyScopes(string $parent): string
468+
private static function exportPropertyScopes(string $parent, array $propertyScopes): string
448469
{
449-
$propertyScopes = Hydrator::$propertyScopes[$parent] ??= Hydrator::getPropertyScopes($parent);
450470
uksort($propertyScopes, 'strnatcmp');
451471
foreach ($propertyScopes as $k => $v) {
452472
unset($propertyScopes[$k][3]);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy;
13+
14+
abstract class AbstractHooked implements HookedInterface
15+
{
16+
abstract public string $bar { get; }
17+
18+
public int $backed {
19+
get { return $this->backed ??= 234; }
20+
set { $this->backed = $value; }
21+
}
22+
}

src/Symfony/Component/VarExporter/Tests/Fixtures/Hooked.php renamed to src/Symfony/Component/VarExporter/Tests/Fixtures/LazyProxy/Hooked.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Symfony\Component\VarExporter\Tests\Fixtures;
12+
namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy;
1313

1414
class Hooked
1515
{
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy;
13+
14+
interface HookedInterface
15+
{
16+
public string $foo { get; }
17+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
1818
use Symfony\Component\VarExporter\Internal\LazyObjectState;
1919
use Symfony\Component\VarExporter\ProxyHelper;
20-
use Symfony\Component\VarExporter\Tests\Fixtures\Hooked;
2120
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildMagicClass;
2221
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildStdClass;
2322
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildTestClass;
@@ -26,6 +25,7 @@
2625
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\MagicClass;
2726
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ReadOnlyClass;
2827
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\TestClass;
28+
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Hooked;
2929
use Symfony\Component\VarExporter\Tests\Fixtures\SimpleObject;
3030

3131
class LazyGhostTraitTest extends TestCase

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

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
use Symfony\Component\VarExporter\Exception\LogicException;
1919
use Symfony\Component\VarExporter\LazyProxyTrait;
2020
use Symfony\Component\VarExporter\ProxyHelper;
21-
use Symfony\Component\VarExporter\Tests\Fixtures\Hooked;
21+
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\RegularClass;
22+
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\AbstractHooked;
23+
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Hooked;
2224
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\FinalPublicClass;
2325
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\ReadOnlyClass;
2426
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\StringMagicGetClass;
@@ -302,11 +304,12 @@ public function testNormalization()
302304
/**
303305
* @requires PHP 8.4
304306
*/
305-
public function testPropertyHooks()
307+
public function testConcretePropertyHooks()
306308
{
307309
$initialized = false;
308310
$object = $this->createLazyProxy(Hooked::class, function () use (&$initialized) {
309311
$initialized = true;
312+
310313
return new Hooked();
311314
});
312315

@@ -318,6 +321,7 @@ public function testPropertyHooks()
318321
$initialized = false;
319322
$object = $this->createLazyProxy(Hooked::class, function () use (&$initialized) {
320323
$initialized = true;
324+
321325
return new Hooked();
322326
});
323327

@@ -326,6 +330,40 @@ public function testPropertyHooks()
326330
$this->assertSame(345, $object->backed);
327331
}
328332

333+
/**
334+
* @requires PHP 8.4
335+
*/
336+
public function testAbstractPropertyHooks()
337+
{
338+
$initialized = false;
339+
$object = $this->createLazyProxy(AbstractHooked::class, function () use (&$initialized) {
340+
$initialized = true;
341+
342+
return new class extends AbstractHooked {
343+
public string $foo = 'Foo';
344+
public string $bar = 'Bar';
345+
};
346+
});
347+
348+
$this->assertSame('Foo', $object->foo);
349+
$this->assertSame('Bar', $object->bar);
350+
$this->assertTrue($initialized);
351+
352+
$initialized = false;
353+
$object = $this->createLazyProxy(AbstractHooked::class, function () use (&$initialized) {
354+
$initialized = true;
355+
356+
return new class extends AbstractHooked {
357+
public string $foo = 'Foo';
358+
public string $bar = 'Bar';
359+
};
360+
});
361+
362+
$this->assertSame('Bar', $object->bar);
363+
$this->assertSame('Foo', $object->foo);
364+
$this->assertTrue($initialized);
365+
}
366+
329367
/**
330368
* @template T
331369
*

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\VarExporter\Exception\LogicException;
1616
use Symfony\Component\VarExporter\ProxyHelper;
17-
use Symfony\Component\VarExporter\Tests\Fixtures\Hooked;
17+
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Hooked;
1818
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Php82NullStandaloneReturnType;
1919
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\StringMagicGetClass;
2020

0 commit comments

Comments
 (0)