Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit c4ef85d

Browse filesBrowse files
committed
[PropertyInfo] [PropertyAccess] Feature: customize behavior for property hooks on read and write
1 parent dd882db commit c4ef85d
Copy full SHA for c4ef85d

File tree

11 files changed

+177
-12
lines changed
Filter options

11 files changed

+177
-12
lines changed

‎src/Symfony/Component/PropertyAccess/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/CHANGELOG.md
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
* Allow to customize behavior for property hooks on read and write
7+
48
7.0
59
---
610

‎src/Symfony/Component/PropertyAccess/PropertyAccessor.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/PropertyAccessor.php
+26-5Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ class PropertyAccessor implements PropertyAccessorInterface
4747
/** @var int Allow magic __call methods */
4848
public const MAGIC_CALL = ReflectionExtractor::ALLOW_MAGIC_CALL;
4949

50+
public const DO_NOT_BYPASS_HOOKS_ON_PROPERTY = 0;
51+
public const BYPASS_HOOKS_ON_PROPERTY_READ = 1 << 1;
52+
public const BYPASS_HOOKS_ON_PROPERTY_WRITE = 1 << 0;
53+
5054
public const DO_NOT_THROW = 0;
5155
public const THROW_ON_INVALID_INDEX = 1;
5256
public const THROW_ON_INVALID_PROPERTY_PATH = 2;
@@ -84,6 +88,7 @@ public function __construct(
8488
?CacheItemPoolInterface $cacheItemPool = null,
8589
?PropertyReadInfoExtractorInterface $readInfoExtractor = null,
8690
?PropertyWriteInfoExtractorInterface $writeInfoExtractor = null,
91+
private int $byPassHooksOnProperty = self::DO_NOT_BYPASS_HOOKS_ON_PROPERTY,
8792
) {
8893
$this->ignoreInvalidIndices = 0 === ($throw & self::THROW_ON_INVALID_INDEX);
8994
$this->cacheItemPool = $cacheItemPool instanceof NullAdapter ? null : $cacheItemPool; // Replace the NullAdapter by the null value
@@ -414,12 +419,21 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid
414419
throw $e;
415420
}
416421
} elseif (PropertyReadInfo::TYPE_PROPERTY === $type) {
417-
if (!isset($object->$name) && !\array_key_exists($name, (array) $object)) {
422+
$valueSet = false;
423+
$useBypass = $this->byPassHooksOnProperty & self::BYPASS_HOOKS_ON_PROPERTY_READ && $access->hasHook() && !$access->isVirtual();
424+
$valueSeemsToBeNotSet = !isset($object->$name) && !\array_key_exists($name, (array) $object);
425+
if ($valueSeemsToBeNotSet || $useBypass) {
418426
try {
419427
$r = new \ReflectionProperty($class, $name);
428+
if ($valueSeemsToBeNotSet) {
429+
if ($r->isPublic() && !$r->hasType()) {
430+
throw new UninitializedPropertyException(\sprintf('The property "%s::$%s" is not initialized.', $class, $name));
431+
}
432+
}
420433

421-
if ($r->isPublic() && !$r->hasType()) {
422-
throw new UninitializedPropertyException(\sprintf('The property "%s::$%s" is not initialized.', $class, $name));
434+
if ($useBypass) {
435+
$result[self::VALUE] = $r->getRawValue($object);
436+
$valueSet = true;
423437
}
424438
} catch (\ReflectionException $e) {
425439
if (!$ignoreInvalidProperty) {
@@ -428,7 +442,9 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid
428442
}
429443
}
430444

431-
$result[self::VALUE] = $object->$name;
445+
if (!$valueSet) {
446+
$result[self::VALUE] = $object->$name;
447+
}
432448

433449
if (isset($zval[self::REF]) && $access->canBeReference()) {
434450
$result[self::REF] = &$object->$name;
@@ -531,7 +547,12 @@ private function writeProperty(array $zval, string $property, mixed $value, bool
531547
if (PropertyWriteInfo::TYPE_METHOD === $type) {
532548
$object->{$mutator->getName()}($value);
533549
} elseif (PropertyWriteInfo::TYPE_PROPERTY === $type) {
534-
$object->{$mutator->getName()} = $value;
550+
if ($this->byPassHooksOnProperty & self::BYPASS_HOOKS_ON_PROPERTY_WRITE) {
551+
$r = new \ReflectionProperty($class, $mutator->getName());
552+
$r->setRawValue($object, $value);
553+
} else {
554+
$object->{$mutator->getName()} = $value;
555+
}
535556
} elseif (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $type) {
536557
$this->writeCollection($zval, $property, $value, $mutator->getAdderInfo(), $mutator->getRemoverInfo());
537558
}
+23Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
4+
5+
class TestClassHooks
6+
{
7+
public string $hookGetOnly = 'default' {
8+
get => $this->hookGetOnly . ' (hooked on get)';
9+
}
10+
11+
public string $hookSetOnly = 'default' {
12+
set(string $value) {
13+
$this->hookSetOnly = $value . ' (hooked on set)';
14+
}
15+
}
16+
17+
public string $hookBoth = 'default' {
18+
get => $this->hookBoth . ' (hooked on get)';
19+
set(string $value) {
20+
$this->hookBoth = $value . ' (hooked on set)';
21+
}
22+
}
23+
}

‎src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php
+34Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestAdderRemoverInvalidArgumentLength;
2626
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestAdderRemoverInvalidMethods;
2727
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass;
28+
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassHooks;
2829
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassIsWritable;
2930
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall;
3031
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicGet;
@@ -1029,6 +1030,39 @@ public function testIsReadableWithMissingPropertyAndLazyGhost()
10291030
$this->assertFalse($this->propertyAccessor->isReadable($lazyGhost, 'dummy'));
10301031
}
10311032

1033+
public function testBypassHookOnRead()
1034+
{
1035+
$instance = new TestClassHooks();
1036+
$propertyAccessor = new PropertyAccessor(byPassHooksOnProperty: PropertyAccessor::BYPASS_HOOKS_ON_PROPERTY_READ);
1037+
$this->assertSame('default', $propertyAccessor->getValue($instance, 'hookGetOnly'));
1038+
$this->assertSame('default (hooked on get)', $this->propertyAccessor->getValue($instance, 'hookGetOnly'));
1039+
$this->assertSame('default', $propertyAccessor->getValue($instance, 'hookSetOnly'));
1040+
$this->assertSame('default', $this->propertyAccessor->getValue($instance, 'hookSetOnly'));
1041+
$this->assertSame('default', $propertyAccessor->getValue($instance, 'hookBoth'));
1042+
$this->assertSame('default (hooked on get)', $this->propertyAccessor->getValue($instance, 'hookBoth'));
1043+
}
1044+
1045+
public function testBypassHookOnWrite()
1046+
{
1047+
$instance = new TestClassHooks();
1048+
$propertyAccessor = new PropertyAccessor(byPassHooksOnProperty: PropertyAccessor::BYPASS_HOOKS_ON_PROPERTY_WRITE);
1049+
$propertyAccessor->setValue($instance, 'hookGetOnly', 'edited');
1050+
$propertyAccessor->setValue($instance, 'hookSetOnly', 'edited');
1051+
$propertyAccessor->setValue($instance, 'hookBoth', 'edited');
1052+
1053+
$instance2 = new TestClassHooks();
1054+
$this->propertyAccessor->setValue($instance2, 'hookGetOnly', 'edited');
1055+
$this->propertyAccessor->setValue($instance2, 'hookSetOnly', 'edited');
1056+
$this->propertyAccessor->setValue($instance2, 'hookBoth', 'edited');
1057+
1058+
$this->assertSame('edited (hooked on get)', $propertyAccessor->getValue($instance, 'hookGetOnly'));
1059+
$this->assertSame('edited (hooked on get)', $this->propertyAccessor->getValue($instance2, 'hookGetOnly'));
1060+
$this->assertSame('edited', $propertyAccessor->getValue($instance, 'hookSetOnly'));
1061+
$this->assertSame('edited (hooked on set)', $this->propertyAccessor->getValue($instance2, 'hookSetOnly'));
1062+
$this->assertSame('edited (hooked on get)', $propertyAccessor->getValue($instance, 'hookBoth'));
1063+
$this->assertSame('edited (hooked on set) (hooked on get)', $this->propertyAccessor->getValue($instance2, 'hookBoth'));
1064+
}
1065+
10321066
private function createUninitializedObjectPropertyGhost(): UninitializedObjectProperty
10331067
{
10341068
if (!class_exists(ProxyHelper::class)) {

‎src/Symfony/Component/PropertyInfo/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyInfo/CHANGELOG.md
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
* Gather data from property hooks in ReflectionExtractor
7+
48
7.1
59
---
610

‎src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php
+13-5Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -384,11 +384,11 @@ public function getReadInfo(string $class, string $property, array $context = []
384384
}
385385

386386
if ($allowMagicGet && $reflClass->hasMethod('__get') && (($r = $reflClass->getMethod('__get'))->getModifiers() & $this->methodReflectionFlags)) {
387-
return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, $r->returnsReference());
387+
return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, $r->returnsReference(), false, false);
388388
}
389389

390390
if ($hasProperty && (($r = $reflClass->getProperty($property))->getModifiers() & $this->propertyReflectionFlags)) {
391-
return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisiblityForProperty($r), $r->isStatic(), true);
391+
return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisiblityForProperty($r), $r->isStatic(), true, $this->propertyHasHook($r, 'get'), $r->isVirtual());
392392
}
393393

394394
if ($allowMagicCall && $reflClass->hasMethod('__call') && ($reflClass->getMethod('__call')->getModifiers() & $this->methodReflectionFlags)) {
@@ -472,7 +472,7 @@ public function getWriteInfo(string $class, string $property, array $context = [
472472
if ($reflClass->hasProperty($property) && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) {
473473
$reflProperty = $reflClass->getProperty($property);
474474
if (!$reflProperty->isReadOnly()) {
475-
return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, $this->getWriteVisiblityForProperty($reflProperty), $reflProperty->isStatic());
475+
return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, $this->getWriteVisiblityForProperty($reflProperty), $reflProperty->isStatic(), $this->propertyHasHook($reflProperty, 'set'));
476476
}
477477

478478
$errors[] = [\sprintf('The property "%s" in class "%s" is a promoted readonly property.', $property, $reflClass->getName())];
@@ -482,7 +482,7 @@ public function getWriteInfo(string $class, string $property, array $context = [
482482
if ($allowMagicSet) {
483483
[$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__set', 2);
484484
if ($accessible) {
485-
return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, PropertyWriteInfo::VISIBILITY_PUBLIC, false);
485+
return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, PropertyWriteInfo::VISIBILITY_PUBLIC, false, false);
486486
}
487487

488488
$errors[] = $methodAccessibleErrors;
@@ -491,7 +491,7 @@ public function getWriteInfo(string $class, string $property, array $context = [
491491
if ($allowMagicCall) {
492492
[$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__call', 2);
493493
if ($accessible) {
494-
return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, 'set'.$camelized, PropertyWriteInfo::VISIBILITY_PUBLIC, false);
494+
return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, 'set'.$camelized, PropertyWriteInfo::VISIBILITY_PUBLIC, false, false);
495495
}
496496

497497
$errors[] = $methodAccessibleErrors;
@@ -885,6 +885,14 @@ private function isMethodAccessible(\ReflectionClass $class, string $methodName,
885885
return [false, $errors];
886886
}
887887

888+
private function propertyHasHook(\ReflectionProperty $property, string $hookType): bool
889+
{
890+
if (!class_exists(\PropertyHookType::class)) {
891+
return false;
892+
}
893+
return $property->hasHook(\PropertyHookType::from($hookType));
894+
}
895+
888896
/**
889897
* Camelizes a given string.
890898
*/

‎src/Symfony/Component/PropertyInfo/PropertyReadInfo.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyInfo/PropertyReadInfo.php
+12Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public function __construct(
3333
private readonly string $visibility,
3434
private readonly bool $static,
3535
private readonly bool $byRef,
36+
private readonly ?bool $hasHook = null,
37+
private readonly ?bool $isVirtual = null,
3638
) {
3739
}
3840

@@ -69,4 +71,14 @@ public function canBeReference(): bool
6971
{
7072
return $this->byRef;
7173
}
74+
75+
public function hasHook(): ?bool
76+
{
77+
return $this->hasHook;
78+
}
79+
80+
public function isVirtual(): ?bool
81+
{
82+
return $this->isVirtual;
83+
}
7284
}

‎src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public function __construct(
3939
private readonly ?string $name = null,
4040
private readonly ?string $visibility = null,
4141
private readonly ?bool $static = null,
42+
private readonly ?bool $hasHook = null,
4243
) {
4344
}
4445

@@ -116,4 +117,9 @@ public function hasErrors(): bool
116117
{
117118
return (bool) \count($this->errors);
118119
}
120+
121+
public function hasHook(): ?bool
122+
{
123+
return $this->hasHook;
124+
}
119125
}

‎src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php
+23Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy;
2121
use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue;
2222
use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy;
23+
use Symfony\Component\PropertyInfo\Tests\Fixtures\HookedProperties;
2324
use Symfony\Component\PropertyInfo\Tests\Fixtures\NotInstantiable;
2425
use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy;
2526
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71Dummy;
@@ -754,8 +755,14 @@ public function testAsymmetricVisibilityAllowPrivateOnly()
754755
public function testVirtualProperties()
755756
{
756757
$this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualNoSetHook'));
758+
$this->assertTrue($this->extractor->getReadInfo(VirtualProperties::class, 'virtualNoSetHook')->isVirtual());
759+
$this->assertTrue($this->extractor->getReadInfo(VirtualProperties::class, 'virtualNoSetHook')->hasHook());
757760
$this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualSetHookOnly'));
761+
$this->assertTrue($this->extractor->getReadInfo(VirtualProperties::class, 'virtualSetHookOnly')->isVirtual());
762+
$this->assertFalse($this->extractor->getReadInfo(VirtualProperties::class, 'virtualSetHookOnly')->hasHook());
758763
$this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualHook'));
764+
$this->assertTrue($this->extractor->getReadInfo(VirtualProperties::class, 'virtualHook')->isVirtual());
765+
$this->assertTrue($this->extractor->getReadInfo(VirtualProperties::class, 'virtualHook')->hasHook());
759766
$this->assertFalse($this->extractor->isWritable(VirtualProperties::class, 'virtualNoSetHook'));
760767
$this->assertTrue($this->extractor->isWritable(VirtualProperties::class, 'virtualSetHookOnly'));
761768
$this->assertTrue($this->extractor->isWritable(VirtualProperties::class, 'virtualHook'));
@@ -780,6 +787,22 @@ public function testAsymmetricVisibilityMutator(string $property, string $readVi
780787
$this->assertSame($writeVisibility, $writeMutator->getVisibility());
781788
}
782789

790+
/**
791+
* @requires PHP 8.4
792+
*/
793+
public function testHookedProperties()
794+
{
795+
$this->assertTrue($this->extractor->getReadInfo(HookedProperties::class, 'hookGetOnly')->hasHook());
796+
$this->assertFalse($this->extractor->getReadInfo(HookedProperties::class, 'hookGetOnly')->isVirtual());
797+
$this->assertFalse($this->extractor->getWriteInfo(HookedProperties::class, 'hookGetOnly')->hasHook());
798+
$this->assertFalse($this->extractor->getReadInfo(HookedProperties::class, 'hookSetOnly')->hasHook());
799+
$this->assertFalse($this->extractor->getReadInfo(HookedProperties::class, 'hookSetOnly')->isVirtual());
800+
$this->assertTrue($this->extractor->getWriteInfo(HookedProperties::class, 'hookSetOnly')->hasHook());
801+
$this->assertTrue($this->extractor->getReadInfo(HookedProperties::class, 'hookBoth')->hasHook());
802+
$this->assertFalse($this->extractor->getReadInfo(HookedProperties::class, 'hookBoth')->isVirtual());
803+
$this->assertTrue($this->extractor->getWriteInfo(HookedProperties::class, 'hookBoth')->hasHook());
804+
}
805+
783806
public static function provideAsymmetricVisibilityMutator(): iterable
784807
{
785808
yield ['publicPrivate', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PRIVATE];
+30Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\PropertyInfo\Tests\Fixtures;
13+
14+
class HookedProperties
15+
{
16+
public string $hookGetOnly {
17+
get => $this->hookGetOnly . ' (hooked on get)';
18+
}
19+
public string $hookSetOnly {
20+
set(string $value) {
21+
$this->hookSetOnly = $value . ' (hooked on set)';
22+
}
23+
}
24+
public string $hookBoth {
25+
get => $this->hookBoth . ' (hooked on get)';
26+
set(string $value) {
27+
$this->hookBoth = $value . ' (hooked on set)';
28+
}
29+
}
30+
}

‎src/Symfony/Component/PropertyInfo/Tests/Fixtures/VirtualProperties.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyInfo/Tests/Fixtures/VirtualProperties.php
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
class VirtualProperties
1515
{
1616
public bool $virtualNoSetHook { get => true; }
17-
public bool $virtualSetHookOnly { set => $value; }
18-
public bool $virtualHook { get => true; set => $value; }
17+
public bool $virtualSetHookOnly { set (bool $value) { } }
18+
public bool $virtualHook { get => true; set (bool $value) { } }
1919
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.