Skip to content

Commit c4ef85d

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

File tree

11 files changed

+177
-12
lines changed

11 files changed

+177
-12
lines changed

src/Symfony/Component/PropertyAccess/CHANGELOG.md

Lines 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

Lines 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
}
Lines 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

Lines 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

Lines 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

Lines 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

Lines 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

Lines 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

Lines 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];
Lines 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+
}

0 commit comments

Comments
 (0)
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