Skip to content

Commit ec9aab8

Browse files
bug symfony#59884 [VarExporter] Fix support for asymmetric visibility (nicolas-grekas)
This PR was merged into the 6.4 branch. Discussion ---------- [VarExporter] Fix support for asymmetric visibility | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | Fix symfony#59843 | License | MIT Commits ------- 7321bbf [VarExporter] Fix support for asymmetric visibility
2 parents 5d6c29a + 7321bbf commit ec9aab8

File tree

10 files changed

+109
-51
lines changed

10 files changed

+109
-51
lines changed

src/Symfony/Component/VarExporter/Hydrator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ public static function hydrate(object $instance, array $properties = [], array $
6161
$propertyScopes = InternalHydrator::$propertyScopes[$class] ??= InternalHydrator::getPropertyScopes($class);
6262

6363
foreach ($properties as $name => &$value) {
64-
[$scope, $name, $readonlyScope] = $propertyScopes[$name] ?? [$class, $name, $class];
65-
$scopedProperties[$readonlyScope ?? $scope][$name] = &$value;
64+
[$scope, $name, $writeScope] = $propertyScopes[$name] ?? [$class, $name, $class];
65+
$scopedProperties[$writeScope ?? $scope][$name] = &$value;
6666
}
6767
unset($value);
6868
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount
9090
$properties = $serializeProperties;
9191
} else {
9292
foreach ($serializeProperties as $n => $v) {
93-
$c = $reflector->hasProperty($n) && ($p = $reflector->getProperty($n))->isReadOnly() ? $p->class : 'stdClass';
93+
$p = $reflector->hasProperty($n) ? $reflector->getProperty($n) : null;
94+
$c = $p && (\PHP_VERSION_ID >= 80400 ? $p->isProtectedSet() || $p->isPrivateSet() : $p->isReadOnly()) ? $p->class : 'stdClass';
9495
$properties[$c][$n] = $v;
9596
}
9697
}

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

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -271,19 +271,19 @@ public static function getPropertyScopes($class)
271271
$name = $property->name;
272272

273273
if (\ReflectionProperty::IS_PRIVATE & $flags) {
274-
$readonlyScope = null;
275-
if ($flags & \ReflectionProperty::IS_READONLY) {
276-
$readonlyScope = $class;
274+
$writeScope = null;
275+
if (\PHP_VERSION_ID >= 80400 ? $property->isPrivateSet() : ($flags & \ReflectionProperty::IS_READONLY)) {
276+
$writeScope = $class;
277277
}
278-
$propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $readonlyScope, $property];
278+
$propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $writeScope, $property];
279279

280280
continue;
281281
}
282-
$readonlyScope = null;
283-
if ($flags & \ReflectionProperty::IS_READONLY) {
284-
$readonlyScope = $property->class;
282+
$writeScope = null;
283+
if (\PHP_VERSION_ID >= 80400 ? $property->isProtectedSet() || $property->isPrivateSet() : ($flags & \ReflectionProperty::IS_READONLY)) {
284+
$writeScope = $property->class;
285285
}
286-
$propertyScopes[$name] = [$class, $name, $readonlyScope, $property];
286+
$propertyScopes[$name] = [$class, $name, $writeScope, $property];
287287

288288
if (\ReflectionProperty::IS_PROTECTED & $flags) {
289289
$propertyScopes["\0*\0$name"] = $propertyScopes[$name];
@@ -298,9 +298,13 @@ public static function getPropertyScopes($class)
298298
foreach ($r->getProperties(\ReflectionProperty::IS_PRIVATE) as $property) {
299299
if (!$property->isStatic()) {
300300
$name = $property->name;
301-
$readonlyScope = $property->isReadOnly() ? $class : null;
302-
$propertyScopes["\0$class\0$name"] = [$class, $name, $readonlyScope, $property];
303-
$propertyScopes[$name] ??= [$class, $name, $readonlyScope, $property];
301+
if (\PHP_VERSION_ID < 80400) {
302+
$writeScope = $property->isReadOnly() ? $class : null;
303+
} else {
304+
$writeScope = $property->isPrivateSet() ? $class : null;
305+
}
306+
$propertyScopes["\0$class\0$name"] = [$class, $name, $writeScope, $property];
307+
$propertyScopes[$name] ??= [$class, $name, $writeScope, $property];
304308
}
305309
}
306310
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public static function getClassResetters($class)
5858
$propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
5959
}
6060

61-
foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) {
61+
foreach ($propertyScopes as $key => [$scope, $name, $writeScope]) {
6262
$propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name;
6363

6464
if ($k !== $key || "\0$class\0lazyObjectState" === $k) {
@@ -68,7 +68,7 @@ public static function getClassResetters($class)
6868
if ($k === $name && ($propertyScopes[$k][4] ?? false)) {
6969
$hookedProperties[$k] = true;
7070
} else {
71-
$classProperties[$readonlyScope ?? $scope][$name] = $key;
71+
$classProperties[$writeScope ?? $scope][$name] = $key;
7272
}
7373
}
7474

@@ -138,17 +138,17 @@ public static function getParentMethods($class)
138138
return $methods;
139139
}
140140

141-
public static function getScope($propertyScopes, $class, $property, $readonlyScope = null)
141+
public static function getScope($propertyScopes, $class, $property, $writeScope = null)
142142
{
143-
if (null === $readonlyScope && !isset($propertyScopes[$k = "\0$class\0$property"]) && !isset($propertyScopes[$k = "\0*\0$property"])) {
143+
if (null === $writeScope && !isset($propertyScopes[$k = "\0$class\0$property"]) && !isset($propertyScopes[$k = "\0*\0$property"])) {
144144
return null;
145145
}
146146
$frame = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2];
147147

148148
if (\ReflectionProperty::class === $scope = $frame['class'] ?? \Closure::class) {
149149
$scope = $frame['object']->class;
150150
}
151-
if (null === $readonlyScope && '*' === $k[1] && ($class === $scope || (is_subclass_of($class, $scope) && !isset($propertyScopes["\0$scope\0$property"])))) {
151+
if (null === $writeScope && '*' === $k[1] && ($class === $scope || (is_subclass_of($class, $scope) && !isset($propertyScopes["\0$scope\0$property"])))) {
152152
return null;
153153
}
154154

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ public function initialize($instance, $propertyName, $propertyScope)
7171
}
7272
$properties = (array) $instance;
7373
foreach ($values as $key => $value) {
74-
if (!\array_key_exists($key, $properties) && [$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) {
75-
$scope = $readonlyScope ?? ('*' !== $scope ? $scope : $class);
74+
if (!\array_key_exists($key, $properties) && [$scope, $name, $writeScope] = $propertyScopes[$key] ?? null) {
75+
$scope = $writeScope ?? ('*' !== $scope ? $scope : $class);
7676
$accessor = LazyObjectRegistry::$classAccessors[$scope] ??= LazyObjectRegistry::getClassAccessors($scope);
7777
$accessor['set']($instance, $name, $value);
7878

@@ -116,10 +116,10 @@ public function reset($instance): void
116116
$properties = (array) $instance;
117117
$onlyProperties = \is_array($this->initializer) ? $this->initializer : null;
118118

119-
foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) {
119+
foreach ($propertyScopes as $key => [$scope, $name, $writeScope]) {
120120
$propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name;
121121

122-
if ($k === $key && (null !== $readonlyScope || !\array_key_exists($k, $properties))) {
122+
if ($k === $key && (null !== $writeScope || !\array_key_exists($k, $properties))) {
123123
$skippedProperties[$k] = true;
124124
}
125125
}

src/Symfony/Component/VarExporter/LazyGhostTrait.php

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,10 @@ public function initializeLazyObject(): static
113113
$properties = (array) $this;
114114
$propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
115115
foreach ($state->initializer as $key => $initializer) {
116-
if (\array_key_exists($key, $properties) || ![$scope, $name, $readonlyScope] = $propertyScopes[$key] ?? null) {
116+
if (\array_key_exists($key, $properties) || ![$scope, $name, $writeScope] = $propertyScopes[$key] ?? null) {
117117
continue;
118118
}
119-
$scope = $readonlyScope ?? ('*' !== $scope ? $scope : $class);
119+
$scope = $writeScope ?? ('*' !== $scope ? $scope : $class);
120120

121121
if (null === $values) {
122122
if (!\is_array($values = ($state->initializer["\0"])($this, Registry::$defaultProperties[$class]))) {
@@ -161,7 +161,7 @@ public function &__get($name): mixed
161161
$propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
162162
$scope = null;
163163

164-
if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) {
164+
if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) {
165165
$scope = Registry::getScope($propertyScopes, $class, $name);
166166
$state = $this->lazyObjectState ?? null;
167167

@@ -175,7 +175,7 @@ public function &__get($name): mixed
175175
$property = null;
176176
}
177177

178-
if ($property?->isInitialized($this) ?? LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $readonlyScope ?? $scope)) {
178+
if ($property?->isInitialized($this) ?? LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $writeScope ?? $scope)) {
179179
goto get_in_scope;
180180
}
181181
}
@@ -199,7 +199,7 @@ public function &__get($name): mixed
199199

200200
try {
201201
if (null === $scope) {
202-
if (null === $readonlyScope) {
202+
if (null === $writeScope) {
203203
return $this->$name;
204204
}
205205
$value = $this->$name;
@@ -208,7 +208,7 @@ public function &__get($name): mixed
208208
}
209209
$accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
210210

211-
return $accessor['get']($this, $name, null !== $readonlyScope);
211+
return $accessor['get']($this, $name, null !== $writeScope);
212212
} catch (\Error $e) {
213213
if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) {
214214
throw $e;
@@ -223,7 +223,7 @@ public function &__get($name): mixed
223223

224224
$accessor['set']($this, $name, []);
225225

226-
return $accessor['get']($this, $name, null !== $readonlyScope);
226+
return $accessor['get']($this, $name, null !== $writeScope);
227227
} catch (\Error) {
228228
if (preg_match('/^Cannot access uninitialized non-nullable property ([^ ]++) by reference$/', $e->getMessage(), $matches)) {
229229
throw new \Error('Typed property '.$matches[1].' must not be accessed before initialization', $e->getCode(), $e->getPrevious());
@@ -239,15 +239,15 @@ public function __set($name, $value): void
239239
$propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
240240
$scope = null;
241241

242-
if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) {
243-
$scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope);
242+
if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) {
243+
$scope = Registry::getScope($propertyScopes, $class, $name, $writeScope);
244244
$state = $this->lazyObjectState ?? null;
245245

246-
if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"]))
246+
if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"]))
247247
&& LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
248248
) {
249249
if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) {
250-
$state->initialize($this, $name, $readonlyScope ?? $scope);
250+
$state->initialize($this, $name, $writeScope ?? $scope);
251251
}
252252
goto set_in_scope;
253253
}
@@ -274,13 +274,13 @@ public function __isset($name): bool
274274
$propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
275275
$scope = null;
276276

277-
if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) {
277+
if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) {
278278
$scope = Registry::getScope($propertyScopes, $class, $name);
279279
$state = $this->lazyObjectState ?? null;
280280

281281
if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))
282282
&& LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
283-
&& LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $readonlyScope ?? $scope)
283+
&& LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $writeScope ?? $scope)
284284
) {
285285
goto isset_in_scope;
286286
}
@@ -305,15 +305,15 @@ public function __unset($name): void
305305
$propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
306306
$scope = null;
307307

308-
if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) {
309-
$scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope);
308+
if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) {
309+
$scope = Registry::getScope($propertyScopes, $class, $name, $writeScope);
310310
$state = $this->lazyObjectState ?? null;
311311

312-
if ($state && ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"]))
312+
if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"]))
313313
&& LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
314314
) {
315315
if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) {
316-
$state->initialize($this, $name, $readonlyScope ?? $scope);
316+
$state->initialize($this, $name, $writeScope ?? $scope);
317317
}
318318
goto unset_in_scope;
319319
}

src/Symfony/Component/VarExporter/LazyProxyTrait.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public function &__get($name): mixed
8989
$scope = null;
9090
$instance = $this;
9191

92-
if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) {
92+
if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) {
9393
$scope = Registry::getScope($propertyScopes, $class, $name);
9494

9595
if (null === $scope || isset($propertyScopes["\0$scope\0$name"])) {
@@ -122,7 +122,7 @@ public function &__get($name): mixed
122122

123123
try {
124124
if (null === $scope) {
125-
if (null === $readonlyScope && 1 !== $parent) {
125+
if (null === $writeScope && 1 !== $parent) {
126126
return $instance->$name;
127127
}
128128
$value = $instance->$name;
@@ -131,7 +131,7 @@ public function &__get($name): mixed
131131
}
132132
$accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
133133

134-
return $accessor['get']($instance, $name, null !== $readonlyScope || 1 === $parent);
134+
return $accessor['get']($instance, $name, null !== $writeScope || 1 === $parent);
135135
} catch (\Error $e) {
136136
if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) {
137137
throw $e;
@@ -146,7 +146,7 @@ public function &__get($name): mixed
146146

147147
$accessor['set']($instance, $name, []);
148148

149-
return $accessor['get']($instance, $name, null !== $readonlyScope || 1 === $parent);
149+
return $accessor['get']($instance, $name, null !== $writeScope || 1 === $parent);
150150
} catch (\Error) {
151151
throw $e;
152152
}
@@ -159,10 +159,10 @@ public function __set($name, $value): void
159159
$scope = null;
160160
$instance = $this;
161161

162-
if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) {
163-
$scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope);
162+
if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) {
163+
$scope = Registry::getScope($propertyScopes, $class, $name, $writeScope);
164164

165-
if ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"])) {
165+
if ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) {
166166
if ($state = $this->lazyObjectState ?? null) {
167167
$instance = $state->realInstance ??= ($state->initializer)();
168168
}
@@ -227,10 +227,10 @@ public function __unset($name): void
227227
$scope = null;
228228
$instance = $this;
229229

230-
if ([$class, , $readonlyScope] = $propertyScopes[$name] ?? null) {
231-
$scope = Registry::getScope($propertyScopes, $class, $name, $readonlyScope);
230+
if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) {
231+
$scope = Registry::getScope($propertyScopes, $class, $name, $writeScope);
232232

233-
if ($readonlyScope === $scope || isset($propertyScopes["\0$scope\0$name"])) {
233+
if ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) {
234234
if ($state = $this->lazyObjectState ?? null) {
235235
$instance = $state->realInstance ??= ($state->initializer)();
236236
}
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+
class AsymmetricVisibility
15+
{
16+
public private(set) int $foo;
17+
18+
public function __construct(int $foo)
19+
{
20+
$this->foo = $foo;
21+
}
22+
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\MagicClass;
2626
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ReadOnlyClass;
2727
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\TestClass;
28+
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\AsymmetricVisibility;
2829
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Hooked;
2930
use Symfony\Component\VarExporter\Tests\Fixtures\SimpleObject;
3031

@@ -504,6 +505,21 @@ public function testPropertyHooks()
504505
$this->assertSame(345, $object->backed);
505506
}
506507

508+
/**
509+
* @requires PHP 8.4
510+
*/
511+
public function testAsymmetricVisibility()
512+
{
513+
$initialized = false;
514+
$object = $this->createLazyGhost(AsymmetricVisibility::class, function ($instance) use (&$initialized) {
515+
$initialized = true;
516+
517+
$instance->__construct(123);
518+
});
519+
520+
$this->assertSame(123, $object->foo);
521+
}
522+
507523
/**
508524
* @template T
509525
*

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
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\LazyGhost\RegularClass;
2221
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\AbstractHooked;
22+
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\AsymmetricVisibility;
2323
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\Hooked;
2424
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\FinalPublicClass;
2525
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\ReadOnlyClass;
@@ -364,6 +364,21 @@ public function testAbstractPropertyHooks()
364364
$this->assertTrue($initialized);
365365
}
366366

367+
/**
368+
* @requires PHP 8.4
369+
*/
370+
public function testAsymmetricVisibility()
371+
{
372+
$initialized = false;
373+
$object = $this->createLazyProxy(AsymmetricVisibility::class, function () use (&$initialized) {
374+
$initialized = true;
375+
376+
return new AsymmetricVisibility(123);
377+
});
378+
379+
$this->assertSame(123, $object->foo);
380+
}
381+
367382
/**
368383
* @template T
369384
*

0 commit comments

Comments
 (0)