diff --git a/src/Symfony/Component/PropertyAccess/CHANGELOG.md b/src/Symfony/Component/PropertyAccess/CHANGELOG.md index 0dacd605277bf..f0e81d340ac7a 100644 --- a/src/Symfony/Component/PropertyAccess/CHANGELOG.md +++ b/src/Symfony/Component/PropertyAccess/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.3 +--- + + * Allow to customize behavior for property hooks on read and write + 7.0 --- diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 7066e1545e7d6..80d1f6cd9fe23 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -47,6 +47,10 @@ class PropertyAccessor implements PropertyAccessorInterface /** @var int Allow magic __call methods */ public const MAGIC_CALL = ReflectionExtractor::ALLOW_MAGIC_CALL; + public const BYPASS_PROPERTY_HOOK_NONE = 0; + public const BYPASS_PROPERTY_HOOK_WRITE = 1 << 0; + public const BYPASS_PROPERTY_HOOK_READ = 1 << 1; + public const DO_NOT_THROW = 0; public const THROW_ON_INVALID_INDEX = 1; public const THROW_ON_INVALID_PROPERTY_PATH = 2; @@ -67,6 +71,7 @@ class PropertyAccessor implements PropertyAccessorInterface private PropertyWriteInfoExtractorInterface $writeInfoExtractor; private array $readPropertyCache = []; private array $writePropertyCache = []; + private int $bypassPropertyHooks; /** * Should not be used by application code. Use @@ -77,6 +82,9 @@ class PropertyAccessor implements PropertyAccessorInterface * or self::DISALLOW_MAGIC_METHODS for none * @param int $throw A bitwise combination of the THROW_* constants * to specify when exceptions should be thrown + * @param int-mask-of $bypassPropertyHooks A bitwise combination of the BYPASS_PROPERTY_HOOK_* constants + * to specify the hooks you want to bypass, + * or self::BYPASS_PROPERTY_HOOK_NONE for none */ public function __construct( private int $magicMethodsFlags = self::MAGIC_GET | self::MAGIC_SET, @@ -84,12 +92,14 @@ public function __construct( ?CacheItemPoolInterface $cacheItemPool = null, ?PropertyReadInfoExtractorInterface $readInfoExtractor = null, ?PropertyWriteInfoExtractorInterface $writeInfoExtractor = null, + int $bypassPropertyHooks = self::BYPASS_PROPERTY_HOOK_NONE, ) { $this->ignoreInvalidIndices = 0 === ($throw & self::THROW_ON_INVALID_INDEX); $this->cacheItemPool = $cacheItemPool instanceof NullAdapter ? null : $cacheItemPool; // Replace the NullAdapter by the null value $this->ignoreInvalidProperty = 0 === ($throw & self::THROW_ON_INVALID_PROPERTY_PATH); $this->readInfoExtractor = $readInfoExtractor ?? new ReflectionExtractor([], null, null, false); $this->writeInfoExtractor = $writeInfoExtractor ?? new ReflectionExtractor(['set'], null, null, false); + $this->bypassPropertyHooks = \PHP_VERSION_ID >= 80400 ? $bypassPropertyHooks : self::BYPASS_PROPERTY_HOOK_NONE; } public function getValue(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): mixed @@ -414,13 +424,20 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid throw $e; } } elseif (PropertyReadInfo::TYPE_PROPERTY === $type) { - if (!isset($object->$name) && !\array_key_exists($name, (array) $object)) { + $valueSet = false; + $bypassHooks = $this->bypassPropertyHooks & self::BYPASS_PROPERTY_HOOK_READ && $access->hasHook() && !$access->isVirtual(); + $initialValueNotSet = !isset($object->$name) && !\array_key_exists($name, (array) $object); + if ($initialValueNotSet || $bypassHooks) { try { $r = new \ReflectionProperty($class, $name); - - if ($r->isPublic() && !$r->hasType()) { + if ($initialValueNotSet && $r->isPublic() && !$r->hasType()) { throw new UninitializedPropertyException(\sprintf('The property "%s::$%s" is not initialized.', $class, $name)); } + + if ($bypassHooks) { + $result[self::VALUE] = $r->getRawValue($object); + $valueSet = true; + } } catch (\ReflectionException $e) { if (!$ignoreInvalidProperty) { throw new NoSuchPropertyException(\sprintf('Can\'t get a way to read the property "%s" in class "%s".', $property, $class)); @@ -428,7 +445,9 @@ private function readProperty(array $zval, string $property, bool $ignoreInvalid } } - $result[self::VALUE] = $object->$name; + if (!$valueSet) { + $result[self::VALUE] = $object->$name; + } if (isset($zval[self::REF]) && $access->canBeReference()) { $result[self::REF] = &$object->$name; @@ -531,7 +550,12 @@ private function writeProperty(array $zval, string $property, mixed $value, bool if (PropertyWriteInfo::TYPE_METHOD === $type) { $object->{$mutator->getName()}($value); } elseif (PropertyWriteInfo::TYPE_PROPERTY === $type) { - $object->{$mutator->getName()} = $value; + if ($this->bypassPropertyHooks & self::BYPASS_PROPERTY_HOOK_WRITE) { + $r = new \ReflectionProperty($class, $mutator->getName()); + $r->setRawValue($object, $value); + } else { + $object->{$mutator->getName()} = $value; + } } elseif (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $type) { $this->writeCollection($zval, $property, $value, $mutator->getAdderInfo(), $mutator->getRemoverInfo()); } diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassHooks.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassHooks.php new file mode 100644 index 0000000000000..594ec83afe336 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassHooks.php @@ -0,0 +1,23 @@ + $this->hookGetOnly . ' (hooked on get)'; + } + + public string $hookSetOnly = 'default' { + set(string $value) { + $this->hookSetOnly = $value . ' (hooked on set)'; + } + } + + public string $hookBoth = 'default' { + get => $this->hookBoth . ' (hooked on get)'; + set(string $value) { + $this->hookBoth = $value . ' (hooked on set)'; + } + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index bb8043d5d45bd..3f837860d3e2c 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -26,6 +26,7 @@ use Symfony\Component\PropertyAccess\Tests\Fixtures\TestAdderRemoverInvalidArgumentLength; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestAdderRemoverInvalidMethods; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassHooks; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassIsWritable; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicGet; @@ -1030,6 +1031,45 @@ public function testIsReadableWithMissingPropertyAndLazyGhost() $this->assertFalse($this->propertyAccessor->isReadable($lazyGhost, 'dummy')); } + /** + * @requires PHP 8.4 + */ + public function testBypassHookOnRead() + { + $instance = new TestClassHooks(); + $bypassingPropertyAccessor = new PropertyAccessor(bypassPropertyHooks: PropertyAccessor::BYPASS_PROPERTY_HOOK_READ); + $this->assertSame('default', $bypassingPropertyAccessor->getValue($instance, 'hookGetOnly')); + $this->assertSame('default (hooked on get)', $this->propertyAccessor->getValue($instance, 'hookGetOnly')); + $this->assertSame('default', $bypassingPropertyAccessor->getValue($instance, 'hookSetOnly')); + $this->assertSame('default', $this->propertyAccessor->getValue($instance, 'hookSetOnly')); + $this->assertSame('default', $bypassingPropertyAccessor->getValue($instance, 'hookBoth')); + $this->assertSame('default (hooked on get)', $this->propertyAccessor->getValue($instance, 'hookBoth')); + } + + /** + * @requires PHP 8.4 + */ + public function testBypassHookOnWrite() + { + $instance = new TestClassHooks(); + $bypassingPropertyAccessor = new PropertyAccessor(bypassPropertyHooks: PropertyAccessor::BYPASS_PROPERTY_HOOK_WRITE); + $bypassingPropertyAccessor->setValue($instance, 'hookGetOnly', 'edited'); + $bypassingPropertyAccessor->setValue($instance, 'hookSetOnly', 'edited'); + $bypassingPropertyAccessor->setValue($instance, 'hookBoth', 'edited'); + + $instance2 = new TestClassHooks(); + $this->propertyAccessor->setValue($instance2, 'hookGetOnly', 'edited'); + $this->propertyAccessor->setValue($instance2, 'hookSetOnly', 'edited'); + $this->propertyAccessor->setValue($instance2, 'hookBoth', 'edited'); + + $this->assertSame('edited (hooked on get)', $bypassingPropertyAccessor->getValue($instance, 'hookGetOnly')); + $this->assertSame('edited (hooked on get)', $this->propertyAccessor->getValue($instance2, 'hookGetOnly')); + $this->assertSame('edited', $bypassingPropertyAccessor->getValue($instance, 'hookSetOnly')); + $this->assertSame('edited (hooked on set)', $this->propertyAccessor->getValue($instance2, 'hookSetOnly')); + $this->assertSame('edited (hooked on get)', $bypassingPropertyAccessor->getValue($instance, 'hookBoth')); + $this->assertSame('edited (hooked on set) (hooked on get)', $this->propertyAccessor->getValue($instance2, 'hookBoth')); + } + private function createUninitializedObjectPropertyGhost(): UninitializedObjectProperty { if (\PHP_VERSION_ID < 80400) { diff --git a/src/Symfony/Component/PropertyAccess/composer.json b/src/Symfony/Component/PropertyAccess/composer.json index 376ee7e1afd0d..4ec1d5be540fe 100644 --- a/src/Symfony/Component/PropertyAccess/composer.json +++ b/src/Symfony/Component/PropertyAccess/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": ">=8.2", - "symfony/property-info": "^6.4|^7.0" + "symfony/property-info": "^7.3" }, "require-dev": { "symfony/cache": "^6.4|^7.0" diff --git a/src/Symfony/Component/PropertyInfo/CHANGELOG.md b/src/Symfony/Component/PropertyInfo/CHANGELOG.md index 9f3cf35706b90..9d7e3fc82933c 100644 --- a/src/Symfony/Component/PropertyInfo/CHANGELOG.md +++ b/src/Symfony/Component/PropertyInfo/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.3 --- + * Gather data from property hooks in ReflectionExtractor * Add support for `non-positive-int`, `non-negative-int` and `non-zero-int` PHPStan types to `PhpStanExtractor` * Add `PropertyDescriptionExtractorInterface` to `PhpStanExtractor` * Deprecate the `Type` class, use `Symfony\Component\TypeInfo\Type` class from `symfony/type-info` instead diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 39b16caeb86e3..ddc0f9eba23bc 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -392,12 +392,12 @@ public function getReadInfo(string $class, string $property, array $context = [] return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $getsetter, $this->getReadVisibilityForMethod($method), $method->isStatic(), false); } - if ($allowMagicGet && $reflClass->hasMethod('__get') && (($r = $reflClass->getMethod('__get'))->getModifiers() & $this->methodReflectionFlags)) { - return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, $r->returnsReference()); + if ($hasProperty && (($r = $reflClass->getProperty($property))->getModifiers() & $this->propertyReflectionFlags)) { + return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisibilityForProperty($r), $r->isStatic(), true, $this->propertyHasHook($r, 'get'), $this->propertyIsVirtual($r)); } - if ($hasProperty && (($r = $reflClass->getProperty($property))->getModifiers() & $this->propertyReflectionFlags)) { - return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisibilityForProperty($r), $r->isStatic(), true); + if ($allowMagicGet && $reflClass->hasMethod('__get') && (($r = $reflClass->getMethod('__get'))->getModifiers() & $this->methodReflectionFlags)) { + return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, $r->returnsReference(), false, false); } if ($allowMagicCall && $reflClass->hasMethod('__call') && ($reflClass->getMethod('__call')->getModifiers() & $this->methodReflectionFlags)) { @@ -481,7 +481,7 @@ public function getWriteInfo(string $class, string $property, array $context = [ if ($reflClass->hasProperty($property) && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) { $reflProperty = $reflClass->getProperty($property); if (!$reflProperty->isReadOnly()) { - return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, $this->getWriteVisibilityForProperty($reflProperty), $reflProperty->isStatic()); + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, $this->getWriteVisibilityForProperty($reflProperty), $reflProperty->isStatic(), $this->propertyHasHook($reflProperty, 'set')); } $errors[] = [\sprintf('The property "%s" in class "%s" is a promoted readonly property.', $property, $reflClass->getName())]; @@ -491,7 +491,7 @@ public function getWriteInfo(string $class, string $property, array $context = [ if ($allowMagicSet) { [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__set', 2); if ($accessible) { - return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, PropertyWriteInfo::VISIBILITY_PUBLIC, false); + return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, PropertyWriteInfo::VISIBILITY_PUBLIC, false, false); } $errors[] = $methodAccessibleErrors; @@ -894,6 +894,16 @@ private function isMethodAccessible(\ReflectionClass $class, string $methodName, return [false, $errors]; } + private function propertyHasHook(\ReflectionProperty $property, string $hookType): bool + { + return \PHP_VERSION_ID >= 80400 && $property->hasHook(\PropertyHookType::from($hookType)); + } + + private function propertyIsVirtual(\ReflectionProperty $property): bool + { + return \PHP_VERSION_ID >= 80400 && $property->isVirtual(); + } + /** * Camelizes a given string. */ diff --git a/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php index d006e32483896..5576c03d38d2d 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php +++ b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php @@ -31,6 +31,8 @@ public function __construct( private readonly string $visibility, private readonly bool $static, private readonly bool $byRef, + private readonly ?bool $hasHook = null, + private readonly ?bool $isVirtual = null, ) { } @@ -67,4 +69,14 @@ public function canBeReference(): bool { return $this->byRef; } + + public function hasHook(): ?bool + { + return $this->hasHook; + } + + public function isVirtual(): ?bool + { + return $this->isVirtual; + } } diff --git a/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php index 81ce7eda6d5b0..9892df5b917fc 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php +++ b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php @@ -37,6 +37,7 @@ public function __construct( private readonly ?string $name = null, private readonly ?string $visibility = null, private readonly ?bool $static = null, + private readonly ?bool $hasHook = null, ) { } @@ -114,4 +115,9 @@ public function hasErrors(): bool { return (bool) \count($this->errors); } + + public function hasHook(): ?bool + { + return $this->hasHook; + } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index fbf365ea5f2c4..019bfacb0cf9c 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -21,6 +21,7 @@ use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\HookedProperties; use Symfony\Component\PropertyInfo\Tests\Fixtures\NotInstantiable; use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71Dummy; @@ -788,8 +789,14 @@ public function testAsymmetricVisibilityAllowPrivateOnly() public function testVirtualProperties() { $this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualNoSetHook')); + $this->assertTrue($this->extractor->getReadInfo(VirtualProperties::class, 'virtualNoSetHook')->isVirtual()); + $this->assertTrue($this->extractor->getReadInfo(VirtualProperties::class, 'virtualNoSetHook')->hasHook()); $this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualSetHookOnly')); + $this->assertTrue($this->extractor->getReadInfo(VirtualProperties::class, 'virtualSetHookOnly')->isVirtual()); + $this->assertFalse($this->extractor->getReadInfo(VirtualProperties::class, 'virtualSetHookOnly')->hasHook()); $this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualHook')); + $this->assertTrue($this->extractor->getReadInfo(VirtualProperties::class, 'virtualHook')->isVirtual()); + $this->assertTrue($this->extractor->getReadInfo(VirtualProperties::class, 'virtualHook')->hasHook()); $this->assertFalse($this->extractor->isWritable(VirtualProperties::class, 'virtualNoSetHook')); $this->assertTrue($this->extractor->isWritable(VirtualProperties::class, 'virtualSetHookOnly')); $this->assertTrue($this->extractor->isWritable(VirtualProperties::class, 'virtualHook')); @@ -814,6 +821,22 @@ public function testAsymmetricVisibilityMutator(string $property, string $readVi $this->assertSame($writeVisibility, $writeMutator->getVisibility()); } + /** + * @requires PHP 8.4 + */ + public function testHookedProperties() + { + $this->assertTrue($this->extractor->getReadInfo(HookedProperties::class, 'hookGetOnly')->hasHook()); + $this->assertFalse($this->extractor->getReadInfo(HookedProperties::class, 'hookGetOnly')->isVirtual()); + $this->assertFalse($this->extractor->getWriteInfo(HookedProperties::class, 'hookGetOnly')->hasHook()); + $this->assertFalse($this->extractor->getReadInfo(HookedProperties::class, 'hookSetOnly')->hasHook()); + $this->assertFalse($this->extractor->getReadInfo(HookedProperties::class, 'hookSetOnly')->isVirtual()); + $this->assertTrue($this->extractor->getWriteInfo(HookedProperties::class, 'hookSetOnly')->hasHook()); + $this->assertTrue($this->extractor->getReadInfo(HookedProperties::class, 'hookBoth')->hasHook()); + $this->assertFalse($this->extractor->getReadInfo(HookedProperties::class, 'hookBoth')->isVirtual()); + $this->assertTrue($this->extractor->getWriteInfo(HookedProperties::class, 'hookBoth')->hasHook()); + } + public static function provideAsymmetricVisibilityMutator(): iterable { yield ['publicPrivate', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PRIVATE]; diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/HookedProperties.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/HookedProperties.php new file mode 100644 index 0000000000000..106319210b8e7 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/HookedProperties.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +class HookedProperties +{ + public string $hookGetOnly { + get => $this->hookGetOnly . ' (hooked on get)'; + } + public string $hookSetOnly { + set(string $value) { + $this->hookSetOnly = $value . ' (hooked on set)'; + } + } + public string $hookBoth { + get => $this->hookBoth . ' (hooked on get)'; + set(string $value) { + $this->hookBoth = $value . ' (hooked on set)'; + } + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/VirtualProperties.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/VirtualProperties.php index 38c6d17082ffe..f90efc7ec0f9a 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/VirtualProperties.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/VirtualProperties.php @@ -14,6 +14,6 @@ class VirtualProperties { public bool $virtualNoSetHook { get => true; } - public bool $virtualSetHookOnly { set => $value; } - public bool $virtualHook { get => true; set => $value; } + public bool $virtualSetHookOnly { set (bool $value) { } } + public bool $virtualHook { get => true; set (bool $value) { } } } 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