Skip to content

[PropertyAccess][PropertyInfo] customize behavior for property hooks on read and write #59246

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 7.3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Symfony/Component/PropertyAccess/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.3
---

* Allow to customize behavior for property hooks on read and write

7.0
---

Expand Down
34 changes: 29 additions & 5 deletions src/Symfony/Component/PropertyAccess/PropertyAccessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
/** @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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constants for a bit mask are generally defined in increasing order (so that new cases are added at the end of the list). This reverse order is unusual to me

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;
Expand All @@ -67,6 +71,7 @@
private PropertyWriteInfoExtractorInterface $writeInfoExtractor;
private array $readPropertyCache = [];
private array $writePropertyCache = [];
private int $bypassPropertyHooks;

/**
* Should not be used by application code. Use
Expand All @@ -77,19 +82,24 @@
* 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<self::BYPASS_PROPERTY_HOOK_*> $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,
int $throw = self::THROW_ON_INVALID_PROPERTY_PATH,
?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
Expand Down Expand Up @@ -414,21 +424,30 @@
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);

Check failure on line 438 in src/Symfony/Component/PropertyAccess/PropertyAccessor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/PropertyAccess/PropertyAccessor.php:438:60: UndefinedMethod: Method ReflectionProperty::getRawValue does not exist (see https://psalm.dev/022)

Check failure on line 438 in src/Symfony/Component/PropertyAccess/PropertyAccessor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/PropertyAccess/PropertyAccessor.php:438:60: UndefinedMethod: Method ReflectionProperty::getRawValue does not exist (see https://psalm.dev/022)
$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));
}
}
}

$result[self::VALUE] = $object->$name;
if (!$valueSet) {
$result[self::VALUE] = $object->$name;
}

if (isset($zval[self::REF]) && $access->canBeReference()) {
$result[self::REF] = &$object->$name;
Expand Down Expand Up @@ -531,7 +550,12 @@
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);

Check failure on line 555 in src/Symfony/Component/PropertyAccess/PropertyAccessor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/PropertyAccess/PropertyAccessor.php:555:29: UndefinedMethod: Method ReflectionProperty::setRawValue does not exist (see https://psalm.dev/022)

Check failure on line 555 in src/Symfony/Component/PropertyAccess/PropertyAccessor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/PropertyAccess/PropertyAccessor.php:555:29: UndefinedMethod: Method ReflectionProperty::setRawValue does not exist (see https://psalm.dev/022)
} else {
$object->{$mutator->getName()} = $value;
}
} elseif (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $type) {
$this->writeCollection($zval, $property, $value, $mutator->getAdderInfo(), $mutator->getRemoverInfo());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Symfony\Component\PropertyAccess\Tests\Fixtures;

class TestClassHooks
{
public string $hookGetOnly = 'default' {
get => $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)';
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/PropertyAccess/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/PropertyInfo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,12 +392,12 @@
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);
}
Comment on lines +395 to 401
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these ifs has been inverted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See that comment #59246 (comment)


if ($allowMagicCall && $reflClass->hasMethod('__call') && ($reflClass->getMethod('__call')->getModifiers() & $this->methodReflectionFlags)) {
Expand Down Expand Up @@ -481,7 +481,7 @@
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())];
Expand All @@ -491,7 +491,7 @@
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;
Expand Down Expand Up @@ -894,6 +894,16 @@
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.
*/
Expand Down Expand Up @@ -975,7 +985,7 @@
private function getWriteVisibilityForProperty(\ReflectionProperty $reflectionProperty): string
{
if (\PHP_VERSION_ID >= 80400) {
if ($reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) {

Check failure on line 988 in src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php:988:83: UndefinedClass: Class, interface or enum named PropertyHookType does not exist (see https://psalm.dev/019)

Check failure on line 988 in src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php:988:38: UndefinedMethod: Method ReflectionProperty::isVirtual does not exist (see https://psalm.dev/022)

Check failure on line 988 in src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedClass

src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php:988:83: UndefinedClass: Class, interface or enum named PropertyHookType does not exist (see https://psalm.dev/019)

Check failure on line 988 in src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php:988:38: UndefinedMethod: Method ReflectionProperty::isVirtual does not exist (see https://psalm.dev/022)
return PropertyWriteInfo::VISIBILITY_PRIVATE;
}

Expand Down
12 changes: 12 additions & 0 deletions src/Symfony/Component/PropertyInfo/PropertyReadInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
}

Expand Down Expand Up @@ -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;
}
}
6 changes: 6 additions & 0 deletions src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
}

Expand Down Expand Up @@ -114,4 +115,9 @@ public function hasErrors(): bool
{
return (bool) \count($this->errors);
}

public function hasHook(): ?bool
{
return $this->hasHook;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'));
Expand All @@ -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];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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)';
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) { } }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated this fixtures because PHP does not report these properties as Virtual, see reproducer

}
Loading