diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 141233f7afa0e..ca35d5234110a 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -617,12 +617,20 @@ private function isAllowedProperty(string $class, string $property, bool $writeA try { $reflectionProperty = new \ReflectionProperty($class, $property); - if (\PHP_VERSION_ID >= 80100 && $writeAccessRequired && $reflectionProperty->isReadOnly()) { - return false; - } + if ($writeAccessRequired) { + if (\PHP_VERSION_ID >= 80100 && $reflectionProperty->isReadOnly()) { + return false; + } + + if (\PHP_VERSION_ID >= 80400) { + if ($reflectionProperty->isProtectedSet() || $reflectionProperty->isPrivateSet()) { + return false; + } - if (\PHP_VERSION_ID >= 80400 && $writeAccessRequired && ($reflectionProperty->isProtectedSet() || $reflectionProperty->isPrivateSet())) { - return false; + if ($reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) { + return false; + } + } } return (bool) ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags); diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index e659cfda7784a..3226755916cfe 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -28,6 +28,7 @@ use Symfony\Component\PropertyInfo\Tests\Fixtures\Php7Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php7ParentDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php81Dummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\HookedProperties; use Symfony\Component\PropertyInfo\Type; /** @@ -699,4 +700,24 @@ public function testAsymmetricVisibility() $this->assertFalse($this->extractor->isWritable(AsymmetricVisibility::class, 'publicProtected')); $this->assertFalse($this->extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate')); } + + /** + * @requires PHP 8.4 + */ + public function testHookedProperties() + { + $this->assertTrue($this->extractor->isReadable(HookedProperties::class, 'virtualNoSetHook')); + $this->assertTrue($this->extractor->isReadable(HookedProperties::class, 'virtualSetHookOnly')); + $this->assertTrue($this->extractor->isReadable(HookedProperties::class, 'virtualHook')); + $this->assertFalse($this->extractor->isWritable(HookedProperties::class, 'virtualNoSetHook')); + $this->assertTrue($this->extractor->isWritable(HookedProperties::class, 'virtualSetHookOnly')); + $this->assertTrue($this->extractor->isWritable(HookedProperties::class, 'virtualHook')); + + $this->assertTrue($this->extractor->isReadable(HookedProperties::class, 'backedNoSetHook')); + $this->assertTrue($this->extractor->isReadable(HookedProperties::class, 'backedSetHookOnly')); + $this->assertTrue($this->extractor->isReadable(HookedProperties::class, 'backedHook')); + $this->assertTrue($this->extractor->isWritable(HookedProperties::class, 'backedNoSetHook')); + $this->assertTrue($this->extractor->isWritable(HookedProperties::class, 'backedSetHookOnly')); + $this->assertTrue($this->extractor->isWritable(HookedProperties::class, 'backedHook')); + } } 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..5fb0ce4bd62c8 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/HookedProperties.php @@ -0,0 +1,23 @@ + + * + * 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 bool $virtualNoSetHook { get => true; } + public bool $virtualSetHookOnly { set => $value; } + public bool $virtualHook { get => true; set => $value; } + + public bool $backedNoSetHook = true { get => !$this->backedNoSetHook; } + public bool $backedSetHookOnly = true { set => $this->backedSetHookOnly = $value; } + public bool $backedHook = true { get => !$this->backedHook; set => $this->backedHook = $value; } +}