diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index f8ee3d7715273..970a7ebe35b88 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -713,6 +713,18 @@ private function isAllowedProperty(string $class, string $property, bool $writeA return false; } + if (\PHP_VERSION_ID >= 80400) { + // If the property is virtual and has no setter, it's not writable. + if ($writeAccessRequired && $reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) { + return false; + } + + // If the property has private or protected setter, it's not writable + if ($writeAccessRequired && ($reflectionProperty->isPrivateSet() || $reflectionProperty->isProtectedSet())) { + return false; + } + } + return (bool) ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags); } catch (\ReflectionException) { // Return false if the property doesn't exist @@ -951,6 +963,23 @@ private function getReadVisiblityForMethod(\ReflectionMethod $reflectionMethod): private function getWriteVisiblityForProperty(\ReflectionProperty $reflectionProperty): string { + if (\PHP_VERSION_ID >= 80400) { + // If the property is virtual and has no setter, it's private + if ($reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) { + return PropertyWriteInfo::VISIBILITY_PRIVATE; + } + + // If the property has private setter, it's not writable + if ($reflectionProperty->isPrivateSet()) { + return PropertyWriteInfo::VISIBILITY_PRIVATE; + } + + // If the property has protected setter, it's protected + if ($reflectionProperty->isProtectedSet()) { + return PropertyWriteInfo::VISIBILITY_PROTECTED; + } + } + if ($reflectionProperty->isPrivate()) { return PropertyWriteInfo::VISIBILITY_PRIVATE; } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index 92424cd3fdc9c..d6077ca535987 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -30,6 +30,7 @@ use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php81Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php82Dummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\Php84Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\SnakeCaseDummy; use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\TypeInfo\Type; @@ -383,60 +384,75 @@ public static function provideLegacyDefaultValue() /** * @dataProvider getReadableProperties */ - public function testIsReadable($property, $expected) + public function testIsReadable($class, $property, $expected) { $this->assertSame( $expected, - $this->extractor->isReadable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property, []) + $this->extractor->isReadable($class, $property, []) ); } public static function getReadableProperties() { return [ - ['bar', false], - ['baz', false], - ['parent', true], - ['a', true], - ['b', false], - ['c', true], - ['d', true], - ['e', false], - ['f', false], - ['Id', true], - ['id', true], - ['Guid', true], - ['guid', false], - ['element', false], + [Dummy::class, 'bar', false], + [Dummy::class, 'baz', false], + [Dummy::class, 'parent', true], + [Dummy::class, 'a', true], + [Dummy::class, 'b', false], + [Dummy::class, 'c', true], + [Dummy::class, 'd', true], + [Dummy::class, 'e', false], + [Dummy::class, 'f', false], + [Dummy::class, 'Id', true], + [Dummy::class, 'id', true], + [Dummy::class, 'Guid', true], + [Dummy::class, 'guid', false], + [Dummy::class, 'element', false], + [Php84Dummy::class, 'publicPrivateSet', true], + [Php84Dummy::class, 'publicProtectedSet', true], + [Php84Dummy::class, 'publicPublicSet', true], + [Php84Dummy::class, 'protectedPrivateSet', false], + [Php84Dummy::class, 'virtualNoSetHook', true], + // Set-only properties can be still read. + [Php84Dummy::class, 'virtualSetHookOnly', true], + [Php84Dummy::class, 'virtualHook', true], ]; } /** * @dataProvider getWritableProperties */ - public function testIsWritable($property, $expected) + public function testIsWritable($class, $property, $expected) { $this->assertSame( $expected, - $this->extractor->isWritable(Dummy::class, $property, []) + $this->extractor->isWritable($class, $property, []) ); } public static function getWritableProperties() { return [ - ['bar', false], - ['baz', false], - ['parent', true], - ['a', false], - ['b', true], - ['c', false], - ['d', false], - ['e', true], - ['f', true], - ['Id', false], - ['Guid', true], - ['guid', false], + [Dummy::class, 'bar', false], + [Dummy::class, 'baz', false], + [Dummy::class, 'parent', true], + [Dummy::class, 'a', false], + [Dummy::class, 'b', true], + [Dummy::class, 'c', false], + [Dummy::class, 'd', false], + [Dummy::class, 'e', true], + [Dummy::class, 'f', true], + [Dummy::class, 'Id', false], + [Dummy::class, 'Guid', true], + [Dummy::class, 'guid', false], + [Php84Dummy::class, 'publicPrivateSet', false], + [Php84Dummy::class, 'publicProtectedSet', false], + [Php84Dummy::class, 'publicPublicSet', true], + [Php84Dummy::class, 'protectedPrivateSet', false], + [Php84Dummy::class, 'virtualNoSetHook', false], + [Php84Dummy::class, 'virtualSetHookOnly', true], + [Php84Dummy::class, 'virtualHook', true], ]; } @@ -587,6 +603,13 @@ public static function readAccessorProvider(): array [Dummy::class, 'foo', true, PropertyReadInfo::TYPE_PROPERTY, 'foo', PropertyReadInfo::VISIBILITY_PUBLIC, false], [Php71Dummy::class, 'foo', true, PropertyReadInfo::TYPE_METHOD, 'getFoo', PropertyReadInfo::VISIBILITY_PUBLIC, false], [Php71Dummy::class, 'buz', true, PropertyReadInfo::TYPE_METHOD, 'getBuz', PropertyReadInfo::VISIBILITY_PUBLIC, false], + [Php84Dummy::class, 'publicPrivateSet', true, PropertyReadInfo::TYPE_PROPERTY, 'publicPrivateSet', PropertyReadInfo::VISIBILITY_PUBLIC, false], + [Php84Dummy::class, 'publicProtectedSet', true, PropertyReadInfo::TYPE_PROPERTY, 'publicProtectedSet', PropertyReadInfo::VISIBILITY_PUBLIC, false], + [Php84Dummy::class, 'publicPublicSet', true, PropertyReadInfo::TYPE_PROPERTY, 'publicPublicSet', PropertyReadInfo::VISIBILITY_PUBLIC, false], + [Php84Dummy::class, 'protectedPrivateSet', true, PropertyReadInfo::TYPE_PROPERTY, 'protectedPrivateSet', PropertyReadInfo::VISIBILITY_PROTECTED, false], + [Php84Dummy::class, 'virtualNoSetHook', true, PropertyReadInfo::TYPE_PROPERTY, 'virtualNoSetHook', PropertyReadInfo::VISIBILITY_PUBLIC, false], + [Php84Dummy::class, 'virtualSetHookOnly', true, PropertyReadInfo::TYPE_PROPERTY, 'virtualSetHookOnly', PropertyReadInfo::VISIBILITY_PUBLIC, false], + [Php84Dummy::class, 'virtualHook', true, PropertyReadInfo::TYPE_PROPERTY, 'virtualHook', PropertyReadInfo::VISIBILITY_PUBLIC, false], ]; } @@ -655,6 +678,14 @@ public static function writeMutatorProvider(): array [Php71DummyExtended2::class, 'string', false, false, '', '', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], [Php71DummyExtended2::class, 'string', true, false, '', '', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], [Php71DummyExtended2::class, 'baz', false, true, PropertyWriteInfo::TYPE_ADDER_AND_REMOVER, null, 'addBaz', 'removeBaz', PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php84Dummy::class, 'publicPrivateSet', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'publicPrivateSet', null, null, PropertyWriteInfo::VISIBILITY_PRIVATE, false], + [Php84Dummy::class, 'publicProtectedSet', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'publicProtectedSet', null, null, PropertyWriteInfo::VISIBILITY_PROTECTED, false], + [Php84Dummy::class, 'publicPublicSet', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'publicPublicSet', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php84Dummy::class, 'protectedPrivateSet', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'protectedPrivateSet', null, null, PropertyWriteInfo::VISIBILITY_PRIVATE, false], + // if there is no setter in virtual property, the setter visibility is considered private + [Php84Dummy::class, 'virtualNoSetHook', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'virtualNoSetHook', null, null, PropertyWriteInfo::VISIBILITY_PRIVATE, false], + [Php84Dummy::class, 'virtualSetHookOnly', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'virtualSetHookOnly', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], + [Php84Dummy::class, 'virtualHook', false, true, PropertyWriteInfo::TYPE_PROPERTY, 'virtualHook', null, null, PropertyWriteInfo::VISIBILITY_PUBLIC, false], ]; } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php84Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php84Dummy.php new file mode 100644 index 0000000000000..99f1ad90cee39 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php84Dummy.php @@ -0,0 +1,14 @@ + true; } + public bool $virtualSetHookOnly { set => $value; } + public bool $virtualHook { get => true; set => $value; } +}