From e4901fb4f2da6da09c643fe833a09dd6d7d67ca8 Mon Sep 17 00:00:00 2001 From: Xavier Leune Date: Wed, 18 Dec 2024 11:04:06 +0100 Subject: [PATCH] [PropertyInfo][PropertyAccess] Feature: customize behavior for property hooks on read and write --- .../Component/PropertyAccess/CHANGELOG.md | 5 +++ .../PropertyAccess/PropertyAccessor.php | 34 +++++++++++++--- .../Tests/Fixtures/TestClassHooks.php | 23 +++++++++++ .../Tests/PropertyAccessorTest.php | 40 +++++++++++++++++++ .../Component/PropertyAccess/composer.json | 2 +- .../Component/PropertyInfo/CHANGELOG.md | 1 + .../Extractor/ReflectionExtractor.php | 22 +++++++--- .../PropertyInfo/PropertyReadInfo.php | 12 ++++++ .../PropertyInfo/PropertyWriteInfo.php | 6 +++ .../Extractor/ReflectionExtractorTest.php | 23 +++++++++++ .../Tests/Fixtures/HookedProperties.php | 30 ++++++++++++++ .../Tests/Fixtures/VirtualProperties.php | 4 +- 12 files changed, 188 insertions(+), 14 deletions(-) create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassHooks.php create mode 100644 src/Symfony/Component/PropertyInfo/Tests/Fixtures/HookedProperties.php 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) { } } }