From 9a51d26df4bfc3305585de1b221bd5e2dcfdd9ec Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Thu, 21 Nov 2024 20:45:51 +0800 Subject: [PATCH 1/4] [PropertyInfo] Add Php84Dummy with Property Hooks and Asymmetric Property Visibility --- .../Extractor/ReflectionExtractorTest.php | 91 +++++++++++++------ .../Tests/Fixtures/Php84Dummy.php | 14 +++ 2 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php84Dummy.php 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; } +} From 64feb8daf418b7fdd568cb3ba7c3f027aa0a6481 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Thu, 21 Nov 2024 22:28:49 +0800 Subject: [PATCH 2/4] [PropertyInfo] Implement PHP 8.4 support for isAllowedProperty --- .../Extractor/ReflectionExtractor.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index f8ee3d7715273..5f56ca89090bb 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -713,6 +713,24 @@ private function isAllowedProperty(string $class, string $property, bool $writeA return false; } + // PHP 8.4: Asymmetric Visibility and Property Hooks + $hasAsymmetricVisibilityCapability = method_exists($reflectionProperty, 'isPrivateSet') + && method_exists($reflectionProperty, 'isProtectedSet') + && method_exists($reflectionProperty, 'isVirtual') + && method_exists($reflectionProperty, 'hasHook'); + + if ($hasAsymmetricVisibilityCapability) { + // 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 From e4d754f90b4d3bafed281e3c58ee885f3ae451ae Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Thu, 21 Nov 2024 22:38:39 +0800 Subject: [PATCH 3/4] [PropertyInfo] Implement PHP 8.4 support for getWriteVisiblityForProperty --- .../Extractor/ReflectionExtractor.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 5f56ca89090bb..f6d27c02320ae 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -969,6 +969,24 @@ private function getReadVisiblityForMethod(\ReflectionMethod $reflectionMethod): private function getWriteVisiblityForProperty(\ReflectionProperty $reflectionProperty): string { + // PHP 8.4: Property hooks + if (method_exists($reflectionProperty, 'isVirtual') && method_exists($reflectionProperty, 'hasHook')) { + if ($reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) { + return PropertyWriteInfo::VISIBILITY_PRIVATE; + } + } + + // PHP 8.4: Asymmetric visibility + if (method_exists($reflectionProperty, 'isPrivateSet') && method_exists($reflectionProperty, 'isProtectedSet')) { + if ($reflectionProperty->isPrivateSet()) { + return PropertyWriteInfo::VISIBILITY_PRIVATE; + } + + if ($reflectionProperty->isProtectedSet()) { + return PropertyWriteInfo::VISIBILITY_PROTECTED; + } + } + if ($reflectionProperty->isPrivate()) { return PropertyWriteInfo::VISIBILITY_PRIVATE; } From 9bfec150fbb5293df8cc604c1aae382619121ef3 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Thu, 21 Nov 2024 22:57:23 +0800 Subject: [PATCH 4/4] [ReflectionExtractor] Simplify feature gate with simple "PHP_VERSION_ID" comparison --- .../Extractor/ReflectionExtractor.php | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index f6d27c02320ae..970a7ebe35b88 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -713,13 +713,7 @@ private function isAllowedProperty(string $class, string $property, bool $writeA return false; } - // PHP 8.4: Asymmetric Visibility and Property Hooks - $hasAsymmetricVisibilityCapability = method_exists($reflectionProperty, 'isPrivateSet') - && method_exists($reflectionProperty, 'isProtectedSet') - && method_exists($reflectionProperty, 'isVirtual') - && method_exists($reflectionProperty, 'hasHook'); - - if ($hasAsymmetricVisibilityCapability) { + 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; @@ -969,19 +963,18 @@ private function getReadVisiblityForMethod(\ReflectionMethod $reflectionMethod): private function getWriteVisiblityForProperty(\ReflectionProperty $reflectionProperty): string { - // PHP 8.4: Property hooks - if (method_exists($reflectionProperty, 'isVirtual') && method_exists($reflectionProperty, 'hasHook')) { + 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; } - } - // PHP 8.4: Asymmetric visibility - if (method_exists($reflectionProperty, 'isPrivateSet') && method_exists($reflectionProperty, 'isProtectedSet')) { + // 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; }