From 90d3c96082e24eb7ea1ae8e5eb5675d666a7e0b1 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 13 Oct 2025 22:16:25 +0200 Subject: [PATCH 01/13] misc --- config/set/php84.php | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/config/set/php84.php b/config/set/php84.php index 8bdf9646907..d40c5f1b2b7 100644 --- a/config/set/php84.php +++ b/config/set/php84.php @@ -14,17 +14,15 @@ use Rector\Php84\Rector\Param\ExplicitNullableParamTypeRector; return static function (RectorConfig $rectorConfig): void { - $rectorConfig->rules( - [ - ExplicitNullableParamTypeRector::class, - RoundingModeEnumRector::class, - AddEscapeArgumentRector::class, - NewMethodCallWithoutParenthesesRector::class, - DeprecatedAnnotationToDeprecatedAttributeRector::class, - ForeachToArrayFindRector::class, - ForeachToArrayFindKeyRector::class, - ForeachToArrayAllRector::class, - ForeachToArrayAnyRector::class, - ] - ); + $rectorConfig->rules([ + ExplicitNullableParamTypeRector::class, + RoundingModeEnumRector::class, + AddEscapeArgumentRector::class, + NewMethodCallWithoutParenthesesRector::class, + DeprecatedAnnotationToDeprecatedAttributeRector::class, + ForeachToArrayFindRector::class, + ForeachToArrayFindKeyRector::class, + ForeachToArrayAllRector::class, + ForeachToArrayAnyRector::class, + ]); }; From 195727967c55510ca656a472c37801f60b4ea873 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 13 Oct 2025 22:22:26 +0200 Subject: [PATCH 02/13] [PHP 8.4] Add PropertyHookRector (optional) --- config/set/php84.php | 3 + .../Fixture/skip_different_variable.php.inc | 20 +++ ...p_missmatching_getter_setter_names.php.inc | 18 +++ .../Fixture/some_fixture.php.inc | 36 +++++ .../PropertyHookRectorTest.php | 28 ++++ .../config/configured_rule.php | 13 ++ .../Php84/NodeFactory/PropertyHookFactory.php | 43 ++++++ .../Rector/Class_/PropertyHookRector.php | 133 ++++++++++++++++++ .../ClassMethodAndPropertyAnalyzer.php | 16 ++- src/ValueObject/PhpVersionFeature.php | 6 + 10 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_different_variable.php.inc create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_missmatching_getter_setter_names.php.inc create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/some_fixture.php.inc create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/PropertyHookRectorTest.php create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/config/configured_rule.php create mode 100644 rules/Php84/NodeFactory/PropertyHookFactory.php create mode 100644 rules/Php84/Rector/Class_/PropertyHookRector.php diff --git a/config/set/php84.php b/config/set/php84.php index d40c5f1b2b7..582a9ec5780 100644 --- a/config/set/php84.php +++ b/config/set/php84.php @@ -24,5 +24,8 @@ ForeachToArrayFindKeyRector::class, ForeachToArrayAllRector::class, ForeachToArrayAnyRector::class, + + // optional + // \Rector\Php84\Rector\Class_\PropertyHookRector::class, ]); }; diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_different_variable.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_different_variable.php.inc new file mode 100644 index 00000000000..30cb25da195 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_different_variable.php.inc @@ -0,0 +1,20 @@ +surname; + } + + public function setName(string $name): void + { + $this->surname = ucfirst($name); + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_missmatching_getter_setter_names.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_missmatching_getter_setter_names.php.inc new file mode 100644 index 00000000000..f9eb33fd00f --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_missmatching_getter_setter_names.php.inc @@ -0,0 +1,18 @@ +name; + } + + public function setAnotherName(string $name): void + { + $this->name = ucfirst($name); + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/some_fixture.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/some_fixture.php.inc new file mode 100644 index 00000000000..4b934e01868 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/some_fixture.php.inc @@ -0,0 +1,36 @@ +name; + } + + public function setName(string $name): void + { + $this->name = ucfirst($name); + } +} + +?> +----- + $this->name; + set(string $name) { + $this->name = ucfirst($name); + } + } +} + +?> diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/PropertyHookRectorTest.php b/rules-tests/Php84/Rector/Class_/PropertyHookRector/PropertyHookRectorTest.php new file mode 100644 index 00000000000..9a3360ed62b --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/PropertyHookRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/config/configured_rule.php b/rules-tests/Php84/Rector/Class_/PropertyHookRector/config/configured_rule.php new file mode 100644 index 00000000000..6d5adf4b181 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/config/configured_rule.php @@ -0,0 +1,13 @@ +rule(PropertyHookRector::class); + + $rectorConfig->phpVersion(PhpVersion::PHP_84); +}; diff --git a/rules/Php84/NodeFactory/PropertyHookFactory.php b/rules/Php84/NodeFactory/PropertyHookFactory.php new file mode 100644 index 00000000000..634574d6aaf --- /dev/null +++ b/rules/Php84/NodeFactory/PropertyHookFactory.php @@ -0,0 +1,43 @@ +name->toString(); + + if ($methodName === 'get' . ucfirst($propertyName)) { + $methodName = 'get'; + } elseif ($methodName === 'set' . ucfirst($propertyName)) { + $methodName = 'set'; + } else { + return null; + } + + Assert::notNull($classMethod->stmts); + + $soleStmt = $classMethod->stmts[0]; + + // use sole Expr + if (($soleStmt instanceof Expression || $soleStmt instanceof Return_) && $methodName !== 'set') { + $body = $soleStmt->expr; + } else { + $body = [$soleStmt]; + } + + $setterPropertyHook = new PropertyHook($methodName, $body); + $setterPropertyHook->params = $classMethod->params; + + return $setterPropertyHook; + } +} diff --git a/rules/Php84/Rector/Class_/PropertyHookRector.php b/rules/Php84/Rector/Class_/PropertyHookRector.php new file mode 100644 index 00000000000..5e81d257b04 --- /dev/null +++ b/rules/Php84/Rector/Class_/PropertyHookRector.php @@ -0,0 +1,133 @@ +name; + } + + public function setName(string $name): void + { + $this->name = ucfirst($name); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +final class Product +{ + private string $name + { + get => $this->name; + set($value) => $this->name = ucfirst($value); + } +} + +CODE_SAMPLE + ), + ]); + } + + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + // nothing to hook to + if ($node->getProperties() === []) { + return null; + } + + $classMethodsToRemove = []; + + foreach ($node->getProperties() as $property) { + $propertyName = $this->getName($property); + + $candidateClassMethods = []; + $getterClassMethod = $this->setterGetterFinder->findGetterClassMethod($node, $propertyName); + if ($getterClassMethod instanceof ClassMethod) { + $candidateClassMethods[] = $getterClassMethod; + } + + $setterClassMethod = $this->setterGetterFinder->findSetterClassMethod($node, $propertyName); + if ($setterClassMethod instanceof ClassMethod) { + $candidateClassMethods[] = $setterClassMethod; + } + + foreach ($candidateClassMethods as $candidateClassMethod) { + if (count((array) $candidateClassMethod->stmts) === 1) { + $propertyHook = $this->propertyHookFactory->create($candidateClassMethod, $propertyName); + if (! $propertyHook instanceof PropertyHook) { + continue; + } + + $property->hooks[] = $propertyHook; + $classMethodsToRemove[] = $candidateClassMethod; + } + } + } + + if ($classMethodsToRemove === []) { + return null; + } + + foreach ($node->stmts as $key => $classStmt) { + if (! $classStmt instanceof ClassMethod) { + continue; + } + + if (! in_array($classStmt, $classMethodsToRemove)) { + continue; + } + + unset($node->stmts[$key]); + } + + return $node; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::PROPERTY_HOOKS; + } +} diff --git a/rules/TypeDeclaration/NodeAnalyzer/ClassMethodAndPropertyAnalyzer.php b/rules/TypeDeclaration/NodeAnalyzer/ClassMethodAndPropertyAnalyzer.php index 39680d394eb..a8ca2c47078 100644 --- a/rules/TypeDeclaration/NodeAnalyzer/ClassMethodAndPropertyAnalyzer.php +++ b/rules/TypeDeclaration/NodeAnalyzer/ClassMethodAndPropertyAnalyzer.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\PropertyFetch; -use PhpParser\Node\Expr\Variable; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\Return_; @@ -59,13 +58,16 @@ public function hasOnlyPropertyAssign(ClassMethod $classMethod, string $property $assign = $onlyClassMethodStmt->expr; - if (! $assign->expr instanceof Variable) { - return false; - } + // if (! $assign->expr instanceof Variable) { + // return false; + // } - if (! $this->nodeNameResolver->isName($assign->expr, $propertyName)) { - return false; - } + // if (! $this->nodeNameResolver->isName($assign->expr, $propertyName)) { + // return false; + // } + // + // dump(33123); + // die; $assignVar = $assign->var; if (! $assignVar instanceof PropertyFetch) { diff --git a/src/ValueObject/PhpVersionFeature.php b/src/ValueObject/PhpVersionFeature.php index 20cf7566cef..e9bc08e2a0f 100644 --- a/src/ValueObject/PhpVersionFeature.php +++ b/src/ValueObject/PhpVersionFeature.php @@ -835,6 +835,12 @@ final class PhpVersionFeature */ public const DEPRECATE_ORD_WITH_MULTIBYTE_STRING = PhpVersion::PHP_85; + /** + * @see https://wiki.php.net/rfc/property-hooks + * @var int + */ + public const PROPERTY_HOOKS = PhpVersion::PHP_84; + /** * @see https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_backticks_as_an_alias_for_shell_exec * @var int From 6eeb9e9753850c468526943b6ab3cc68fb782e6d Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 13 Oct 2025 23:43:09 +0200 Subject: [PATCH 03/13] add prent fixture --- .../Fixture/skip_parent_contract.php.inc | 15 +++++++++++++++ .../Source/SomeParentContractInterface.php | 8 ++++++++ 2 files changed, 23 insertions(+) create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_parent_contract.php.inc create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Source/SomeParentContractInterface.php diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_parent_contract.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_parent_contract.php.inc new file mode 100644 index 00000000000..97d019293f3 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_parent_contract.php.inc @@ -0,0 +1,15 @@ +name; + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Source/SomeParentContractInterface.php b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Source/SomeParentContractInterface.php new file mode 100644 index 00000000000..fdd51de208f --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Source/SomeParentContractInterface.php @@ -0,0 +1,8 @@ + Date: Mon, 13 Oct 2025 23:47:14 +0200 Subject: [PATCH 04/13] skip method guarded by parent interface contract --- .../skip_method_with_attributes.php.inc | 22 +++++++ .../Fixture/skip_multi_stmts_getter.php.inc | 15 +++++ .../Fixture/skip_parent_contract.php.inc | 2 +- .../Source/SomeParentContractInterface.php | 2 +- .../Rector/Class_/PropertyHookRector.php | 61 +++++++++++++------ .../ClassMethodAndPropertyAnalyzer.php | 11 ---- 6 files changed, 82 insertions(+), 31 deletions(-) create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_method_with_attributes.php.inc create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_multi_stmts_getter.php.inc diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_method_with_attributes.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_method_with_attributes.php.inc new file mode 100644 index 00000000000..4741a2e3eb7 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_method_with_attributes.php.inc @@ -0,0 +1,22 @@ +name; + } + + #[Required] + public function setName(string $name): void + { + $this->name = ucfirst($name); + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_multi_stmts_getter.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_multi_stmts_getter.php.inc new file mode 100644 index 00000000000..82bf0145be7 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_multi_stmts_getter.php.inc @@ -0,0 +1,15 @@ +name; + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_parent_contract.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_parent_contract.php.inc index 97d019293f3..287c336b6af 100644 --- a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_parent_contract.php.inc +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_parent_contract.php.inc @@ -2,7 +2,7 @@ namespace Rector\Tests\Php84\Rector\Class_\PropertyHookRector\Fixture; -use Php84\Rector\Class_\PropertyHookRector\Source\SomeParentContractInterface; +use Rector\Tests\Php84\Rector\Class_\PropertyHookRector\Source\SomeParentContractInterface; final class SkipParentContract implements SomeParentContractInterface { diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Source/SomeParentContractInterface.php b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Source/SomeParentContractInterface.php index fdd51de208f..ab4920f92cf 100644 --- a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Source/SomeParentContractInterface.php +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Source/SomeParentContractInterface.php @@ -1,6 +1,6 @@ getProperties() as $property) { $propertyName = $this->getName($property); - $candidateClassMethods = []; - $getterClassMethod = $this->setterGetterFinder->findGetterClassMethod($node, $propertyName); - if ($getterClassMethod instanceof ClassMethod) { - $candidateClassMethods[] = $getterClassMethod; - } - - $setterClassMethod = $this->setterGetterFinder->findSetterClassMethod($node, $propertyName); - if ($setterClassMethod instanceof ClassMethod) { - $candidateClassMethods[] = $setterClassMethod; - } + $candidateClassMethods = $this->resolvePropertyHookCandidateClassMethods($node, $propertyName); foreach ($candidateClassMethods as $candidateClassMethod) { - if (count((array) $candidateClassMethod->stmts) === 1) { - $propertyHook = $this->propertyHookFactory->create($candidateClassMethod, $propertyName); - if (! $propertyHook instanceof PropertyHook) { - continue; - } - - $property->hooks[] = $propertyHook; - $classMethodsToRemove[] = $candidateClassMethod; + if (count((array) $candidateClassMethod->stmts) !== 1) { + continue; + } + + // skip attributed methods + if ($candidateClassMethod->attrGroups !== []) { + continue; } + + // avoid parent contract/method override + if ($this->parentClassMethodTypeOverrideGuard->hasParentClassMethod($candidateClassMethod)) { + continue; + } + + $propertyHook = $this->propertyHookFactory->create($candidateClassMethod, $propertyName); + if (! $propertyHook instanceof PropertyHook) { + continue; + } + + $property->hooks[] = $propertyHook; + $classMethodsToRemove[] = $candidateClassMethod; } } @@ -130,4 +135,24 @@ public function provideMinPhpVersion(): int { return PhpVersionFeature::PROPERTY_HOOKS; } + + /** + * @return ClassMethod[] + */ + private function resolvePropertyHookCandidateClassMethods(Class_ $class, string $propertyName): array + { + $candidateClassMethods = []; + + $getterClassMethod = $this->setterGetterFinder->findGetterClassMethod($class, $propertyName); + if ($getterClassMethod instanceof ClassMethod) { + $candidateClassMethods[] = $getterClassMethod; + } + + $setterClassMethod = $this->setterGetterFinder->findSetterClassMethod($class, $propertyName); + if ($setterClassMethod instanceof ClassMethod) { + $candidateClassMethods[] = $setterClassMethod; + } + + return $candidateClassMethods; + } } diff --git a/rules/TypeDeclaration/NodeAnalyzer/ClassMethodAndPropertyAnalyzer.php b/rules/TypeDeclaration/NodeAnalyzer/ClassMethodAndPropertyAnalyzer.php index a8ca2c47078..f9676efad1b 100644 --- a/rules/TypeDeclaration/NodeAnalyzer/ClassMethodAndPropertyAnalyzer.php +++ b/rules/TypeDeclaration/NodeAnalyzer/ClassMethodAndPropertyAnalyzer.php @@ -58,17 +58,6 @@ public function hasOnlyPropertyAssign(ClassMethod $classMethod, string $property $assign = $onlyClassMethodStmt->expr; - // if (! $assign->expr instanceof Variable) { - // return false; - // } - - // if (! $this->nodeNameResolver->isName($assign->expr, $propertyName)) { - // return false; - // } - // - // dump(33123); - // die; - $assignVar = $assign->var; if (! $assignVar instanceof PropertyFetch) { return false; From eb769f2da76105f96398e6c8f7eca3bf49479572 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 14 Oct 2025 09:20:51 +0200 Subject: [PATCH 05/13] narrow --- .../Rector/Class_/PropertyHookRector.php | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/rules/Php84/Rector/Class_/PropertyHookRector.php b/rules/Php84/Rector/Class_/PropertyHookRector.php index e6dfbb7434d..b0a5f4fc33d 100644 --- a/rules/Php84/Rector/Class_/PropertyHookRector.php +++ b/rules/Php84/Rector/Class_/PropertyHookRector.php @@ -85,7 +85,7 @@ public function refactor(Node $node): ?Node foreach ($node->getProperties() as $property) { $propertyName = $this->getName($property); - $candidateClassMethods = $this->resolvePropertyHookCandidateClassMethods($node, $propertyName); + $candidateClassMethods = $this->setterGetterFinder->findGetterAndSetterClassMethods($node, $propertyName); foreach ($candidateClassMethods as $candidateClassMethod) { if (count((array) $candidateClassMethod->stmts) !== 1) { @@ -135,24 +135,4 @@ public function provideMinPhpVersion(): int { return PhpVersionFeature::PROPERTY_HOOKS; } - - /** - * @return ClassMethod[] - */ - private function resolvePropertyHookCandidateClassMethods(Class_ $class, string $propertyName): array - { - $candidateClassMethods = []; - - $getterClassMethod = $this->setterGetterFinder->findGetterClassMethod($class, $propertyName); - if ($getterClassMethod instanceof ClassMethod) { - $candidateClassMethods[] = $getterClassMethod; - } - - $setterClassMethod = $this->setterGetterFinder->findSetterClassMethod($class, $propertyName); - if ($setterClassMethod instanceof ClassMethod) { - $candidateClassMethods[] = $setterClassMethod; - } - - return $candidateClassMethods; - } } From 0c211fd99240473b9161ac7cf2921043d344b5ad Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 14 Oct 2025 09:22:56 +0200 Subject: [PATCH 06/13] skip non-final class --- ...on_final_class_to_avoid_child_break.php.inc | 18 ++++++++++++++++++ .../Php84/Rector/Class_/PropertyHookRector.php | 6 ++++++ 2 files changed, 24 insertions(+) create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_non_final_class_to_avoid_child_break.php.inc diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_non_final_class_to_avoid_child_break.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_non_final_class_to_avoid_child_break.php.inc new file mode 100644 index 00000000000..651b06fa9e6 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_non_final_class_to_avoid_child_break.php.inc @@ -0,0 +1,18 @@ +name; + } + + public function setName(string $name): void + { + $this->name = ucfirst($name); + } +} diff --git a/rules/Php84/Rector/Class_/PropertyHookRector.php b/rules/Php84/Rector/Class_/PropertyHookRector.php index b0a5f4fc33d..c359d273216 100644 --- a/rules/Php84/Rector/Class_/PropertyHookRector.php +++ b/rules/Php84/Rector/Class_/PropertyHookRector.php @@ -8,6 +8,7 @@ use PhpParser\Node\PropertyHook; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; +use Rector\Configuration\Parameter\FeatureFlags; use Rector\Doctrine\CodeQuality\Helper\SetterGetterFinder; use Rector\Php84\NodeFactory\PropertyHookFactory; use Rector\Rector\AbstractRector; @@ -75,6 +76,11 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { + // avoid breaking of child class getter/setter method use + if (! $node->isFinal() && FeatureFlags::treatClassesAsFinal($node) === false) { + return null; + } + // nothing to hook to if ($node->getProperties() === []) { return null; From 3ea10b1a937d55320b3daece6a77c8f99f968a66 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 14 Oct 2025 09:33:46 +0200 Subject: [PATCH 07/13] move SetterAndGetterFinder from doctrine to core, as useful in many extensions --- rector.php | 1 + .../NodeFinder/SetterAndGetterFinder.php | 63 +++++++++++++++++++ .../Rector/Class_/PropertyHookRector.php | 9 ++- 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 rules/Php84/NodeFinder/SetterAndGetterFinder.php diff --git a/rector.php b/rector.php index b24f7fe82f0..f57eb6bacfe 100644 --- a/rector.php +++ b/rector.php @@ -8,6 +8,7 @@ use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector; return RectorConfig::configure() + ->withTreatClassesAsFinal() ->withPreparedSets( deadCode: true, codeQuality: true, diff --git a/rules/Php84/NodeFinder/SetterAndGetterFinder.php b/rules/Php84/NodeFinder/SetterAndGetterFinder.php new file mode 100644 index 00000000000..2fa91e254e1 --- /dev/null +++ b/rules/Php84/NodeFinder/SetterAndGetterFinder.php @@ -0,0 +1,63 @@ +findGetterClassMethod($class, $propertyName); + if ($getterClassMethod instanceof ClassMethod) { + $classMethods[] = $getterClassMethod; + } + + $setterClassMethod = $this->findSetterClassMethod($class, $propertyName); + if ($setterClassMethod instanceof ClassMethod) { + $classMethods[] = $setterClassMethod; + } + + return $classMethods; + } + + public function findGetterClassMethod(Class_ $class, string $propertyName): ?ClassMethod + { + foreach ($class->getMethods() as $classMethod) { + if (! $this->classMethodAndPropertyAnalyzer->hasPropertyFetchReturn($classMethod, $propertyName)) { + continue; + } + + return $classMethod; + } + + return null; + } + + public function findSetterClassMethod(Class_ $class, string $propertyName): ?ClassMethod + { + foreach ($class->getMethods() as $classMethod) { + if (! $this->classMethodAndPropertyAnalyzer->hasOnlyPropertyAssign($classMethod, $propertyName)) { + continue; + } + + return $classMethod; + } + + return null; + } +} diff --git a/rules/Php84/Rector/Class_/PropertyHookRector.php b/rules/Php84/Rector/Class_/PropertyHookRector.php index c359d273216..81639441f30 100644 --- a/rules/Php84/Rector/Class_/PropertyHookRector.php +++ b/rules/Php84/Rector/Class_/PropertyHookRector.php @@ -9,8 +9,8 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use Rector\Configuration\Parameter\FeatureFlags; -use Rector\Doctrine\CodeQuality\Helper\SetterGetterFinder; use Rector\Php84\NodeFactory\PropertyHookFactory; +use Rector\Php84\NodeFinder\SetterAndGetterFinder; use Rector\Rector\AbstractRector; use Rector\ValueObject\PhpVersionFeature; use Rector\VendorLocker\ParentClassMethodTypeOverrideGuard; @@ -24,7 +24,7 @@ final class PropertyHookRector extends AbstractRector implements MinPhpVersionInterface { public function __construct( - private readonly SetterGetterFinder $setterGetterFinder, + private readonly SetterAndGetterFinder $setterAndGetterFinder, private readonly PropertyHookFactory $propertyHookFactory, private readonly ParentClassMethodTypeOverrideGuard $parentClassMethodTypeOverrideGuard ) { @@ -91,7 +91,10 @@ public function refactor(Node $node): ?Node foreach ($node->getProperties() as $property) { $propertyName = $this->getName($property); - $candidateClassMethods = $this->setterGetterFinder->findGetterAndSetterClassMethods($node, $propertyName); + $candidateClassMethods = $this->setterAndGetterFinder->findGetterAndSetterClassMethods( + $node, + $propertyName + ); foreach ($candidateClassMethods as $candidateClassMethod) { if (count((array) $candidateClassMethod->stmts) !== 1) { From 3bafc4b496287cf5b3b4594464ddcbc40c63409e Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 14 Oct 2025 09:43:31 +0200 Subject: [PATCH 08/13] make property public, to comply with getter and setter --- rector.php | 1 - .../Class_/PropertyHookRector/Fixture/some_fixture.php.inc | 2 +- rules/Php84/Rector/Class_/PropertyHookRector.php | 7 ++++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/rector.php b/rector.php index f57eb6bacfe..b24f7fe82f0 100644 --- a/rector.php +++ b/rector.php @@ -8,7 +8,6 @@ use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector; return RectorConfig::configure() - ->withTreatClassesAsFinal() ->withPreparedSets( deadCode: true, codeQuality: true, diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/some_fixture.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/some_fixture.php.inc index 4b934e01868..051d0840fb9 100644 --- a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/some_fixture.php.inc +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/some_fixture.php.inc @@ -25,7 +25,7 @@ namespace Rector\Tests\Php84\Rector\Class_\PropertyHookRector\Fixture; final class SomeFixture { - private string $name { + public string $name { get => $this->name; set(string $name) { $this->name = ucfirst($name); diff --git a/rules/Php84/Rector/Class_/PropertyHookRector.php b/rules/Php84/Rector/Class_/PropertyHookRector.php index 81639441f30..3c673b0243e 100644 --- a/rules/Php84/Rector/Class_/PropertyHookRector.php +++ b/rules/Php84/Rector/Class_/PropertyHookRector.php @@ -4,6 +4,7 @@ namespace Rector\Php84\Rector\Class_; +use PhpParser\Modifiers; use PhpParser\Node; use PhpParser\Node\PropertyHook; use PhpParser\Node\Stmt\Class_; @@ -54,7 +55,7 @@ public function setName(string $name): void <<<'CODE_SAMPLE' final class Product { - private string $name + public string $name { get => $this->name; set($value) => $this->name = ucfirst($value); @@ -116,6 +117,10 @@ public function refactor(Node $node): ?Node continue; } + if (! $property->isPublic()) { + $property->flags = Modifiers::PUBLIC; + } + $property->hooks[] = $propertyHook; $classMethodsToRemove[] = $candidateClassMethod; } From 761a6c3464b5d3404bc623bce4de049bf19e7cb3 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 14 Oct 2025 10:01:02 +0200 Subject: [PATCH 09/13] skip get/set magic --- .../Fixture/skip_get_set_magic.php.inc | 26 +++++++++++++++++++ ...FunctionLikeToFirstClassCallableRector.php | 10 +++---- .../Double/RealToFloatTypeCastRector.php | 2 +- .../Rector/Class_/PropertyHookRector.php | 16 ++++++++++++ 4 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_get_set_magic.php.inc diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_get_set_magic.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_get_set_magic.php.inc new file mode 100644 index 00000000000..e5b35b1204e --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_get_set_magic.php.inc @@ -0,0 +1,26 @@ +name; + } + + public function setName(string $name): void + { + $this->name = ucfirst($name); + } + + public function __get($name) + { + } + + public function __set($name, $value) + { + } +} diff --git a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php index e3bc2fd365c..e8beda1aa68 100644 --- a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php +++ b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php @@ -74,6 +74,11 @@ public function refactor(Node $node): null|CallLike return $callLike; } + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::FIRST_CLASS_CALLABLE_SYNTAX; + } + private function shouldSkip( ArrowFunction|Closure $node, FuncCall|MethodCall|StaticCall $callLike, @@ -240,9 +245,4 @@ private function isChainedCall(FuncCall|MethodCall|StaticCall $callLike): bool return $callLike->var instanceof CallLike; } - - public function provideMinPhpVersion(): int - { - return PhpVersionFeature::FIRST_CLASS_CALLABLE_SYNTAX; - } } diff --git a/rules/Php74/Rector/Double/RealToFloatTypeCastRector.php b/rules/Php74/Rector/Double/RealToFloatTypeCastRector.php index f4ded2b9d84..bddccba25bc 100644 --- a/rules/Php74/Rector/Double/RealToFloatTypeCastRector.php +++ b/rules/Php74/Rector/Double/RealToFloatTypeCastRector.php @@ -4,12 +4,12 @@ namespace Rector\Php74\Rector\Double; -use Rector\Renaming\Rector\Cast\RenameCastRector; use PhpParser\Node; use PhpParser\Node\Expr\Cast\Double; use Rector\Configuration\Deprecation\Contract\DeprecatedInterface; use Rector\Exception\ShouldNotHappenException; use Rector\Rector\AbstractRector; +use Rector\Renaming\Rector\Cast\RenameCastRector; use Rector\ValueObject\PhpVersionFeature; use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; diff --git a/rules/Php84/Rector/Class_/PropertyHookRector.php b/rules/Php84/Rector/Class_/PropertyHookRector.php index 3c673b0243e..5f8fb5887fc 100644 --- a/rules/Php84/Rector/Class_/PropertyHookRector.php +++ b/rules/Php84/Rector/Class_/PropertyHookRector.php @@ -13,6 +13,7 @@ use Rector\Php84\NodeFactory\PropertyHookFactory; use Rector\Php84\NodeFinder\SetterAndGetterFinder; use Rector\Rector\AbstractRector; +use Rector\ValueObject\MethodName; use Rector\ValueObject\PhpVersionFeature; use Rector\VendorLocker\ParentClassMethodTypeOverrideGuard; use Rector\VersionBonding\Contract\MinPhpVersionInterface; @@ -82,6 +83,10 @@ public function refactor(Node $node): ?Node return null; } + if ($this->hasMagicGetSetMethod($node)) { + return null; + } + // nothing to hook to if ($node->getProperties() === []) { return null; @@ -149,4 +154,15 @@ public function provideMinPhpVersion(): int { return PhpVersionFeature::PROPERTY_HOOKS; } + + private function hasMagicGetSetMethod(Class_ $class): bool + { + $magicGetMethod = $class->getMethod(MethodName::__GET); + if ($magicGetMethod instanceof ClassMethod) { + return true; + } + + $magicSetMethod = $class->getMethod(MethodName::__SET); + return $magicSetMethod instanceof ClassMethod; + } } From 951f467e26ea4fd607349efc78580b74556da2eb Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 14 Oct 2025 10:14:15 +0200 Subject: [PATCH 10/13] skip readonly --- .../Fixture/skip_readonly.php.inc | 18 ++++++++++++++++++ .../Php84/Rector/Class_/PropertyHookRector.php | 4 ++++ 2 files changed, 22 insertions(+) create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly.php.inc diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly.php.inc new file mode 100644 index 00000000000..47b1115ffe4 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly.php.inc @@ -0,0 +1,18 @@ +name; + } + + public function setName(string $name): void + { + $this->name = ucfirst($name); + } +} diff --git a/rules/Php84/Rector/Class_/PropertyHookRector.php b/rules/Php84/Rector/Class_/PropertyHookRector.php index 5f8fb5887fc..6e54db53c51 100644 --- a/rules/Php84/Rector/Class_/PropertyHookRector.php +++ b/rules/Php84/Rector/Class_/PropertyHookRector.php @@ -95,6 +95,10 @@ public function refactor(Node $node): ?Node $classMethodsToRemove = []; foreach ($node->getProperties() as $property) { + if ($property->isReadonly()) { + continue; + } + $propertyName = $this->getName($property); $candidateClassMethods = $this->setterAndGetterFinder->findGetterAndSetterClassMethods( From 082bce2a577858efd9e48b0cfd620053dcace875 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 14 Oct 2025 12:51:57 +0200 Subject: [PATCH 11/13] add fixture with getter, ctor and readonly non promoted --- .../Fixture/allow_getter_and_readonly.php.inc | 38 +++++++++++++++++++ ...> skip_readonly_getter_and_setter.php.inc} | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/allow_getter_and_readonly.php.inc rename rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/{skip_readonly.php.inc => skip_readonly_getter_and_setter.php.inc} (88%) diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/allow_getter_and_readonly.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/allow_getter_and_readonly.php.inc new file mode 100644 index 00000000000..db09d2e583f --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/allow_getter_and_readonly.php.inc @@ -0,0 +1,38 @@ +name = $name; + } + + public function getName(): string + { + return $this->name; + } +} + +?> +----- + $this->name; + } + + public function __construct(string $name) + { + $this->name = $name; + } +} + +?> diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_getter_and_setter.php.inc similarity index 88% rename from rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly.php.inc rename to rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_getter_and_setter.php.inc index 47b1115ffe4..2dce01eb3dc 100644 --- a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly.php.inc +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_getter_and_setter.php.inc @@ -2,7 +2,7 @@ namespace Rector\Tests\Php84\Rector\Class_\PropertyHookRector\Fixture; -final class SkipReadonly +final class SkipReadonlyGetterAndSetter { private readonly string $name; From 7001a6dec67fed635677ea66ca9f765de312ae07 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 14 Oct 2025 13:09:54 +0200 Subject: [PATCH 12/13] skip readonly property and readonly class, as hooks cannot be readonly --- .../Fixture/allow_getter_and_readonly.php.inc | 38 ------------------- .../Fixture/skip_getter_and_readonly.php.inc | 18 +++++++++ .../Fixture/skip_readonly_class.php.inc | 18 +++++++++ .../NodeFinder/SetterAndGetterFinder.php | 5 +++ .../Rector/Class_/PropertyHookRector.php | 8 +++- 5 files changed, 47 insertions(+), 40 deletions(-) delete mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/allow_getter_and_readonly.php.inc create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_getter_and_readonly.php.inc create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_class.php.inc diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/allow_getter_and_readonly.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/allow_getter_and_readonly.php.inc deleted file mode 100644 index db09d2e583f..00000000000 --- a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/allow_getter_and_readonly.php.inc +++ /dev/null @@ -1,38 +0,0 @@ -name = $name; - } - - public function getName(): string - { - return $this->name; - } -} - -?> ------ - $this->name; - } - - public function __construct(string $name) - { - $this->name = $name; - } -} - -?> diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_getter_and_readonly.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_getter_and_readonly.php.inc new file mode 100644 index 00000000000..a8dd2371297 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_getter_and_readonly.php.inc @@ -0,0 +1,18 @@ +name = $name; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_class.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_class.php.inc new file mode 100644 index 00000000000..4e4c98fe329 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_class.php.inc @@ -0,0 +1,18 @@ +name; + } + + public function setName(string $name): void + { + $this->name = ucfirst($name); + } +} diff --git a/rules/Php84/NodeFinder/SetterAndGetterFinder.php b/rules/Php84/NodeFinder/SetterAndGetterFinder.php index 2fa91e254e1..2adcc7f15be 100644 --- a/rules/Php84/NodeFinder/SetterAndGetterFinder.php +++ b/rules/Php84/NodeFinder/SetterAndGetterFinder.php @@ -51,6 +51,11 @@ public function findGetterClassMethod(Class_ $class, string $propertyName): ?Cla public function findSetterClassMethod(Class_ $class, string $propertyName): ?ClassMethod { foreach ($class->getMethods() as $classMethod) { + + if ($classMethod->isMagic()) { + continue; + } + if (! $this->classMethodAndPropertyAnalyzer->hasOnlyPropertyAssign($classMethod, $propertyName)) { continue; } diff --git a/rules/Php84/Rector/Class_/PropertyHookRector.php b/rules/Php84/Rector/Class_/PropertyHookRector.php index 6e54db53c51..5cdd9f103eb 100644 --- a/rules/Php84/Rector/Class_/PropertyHookRector.php +++ b/rules/Php84/Rector/Class_/PropertyHookRector.php @@ -78,6 +78,10 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { + if ($node->isReadonly()) { + return null; + } + // avoid breaking of child class getter/setter method use if (! $node->isFinal() && FeatureFlags::treatClassesAsFinal($node) === false) { return null; @@ -95,12 +99,12 @@ public function refactor(Node $node): ?Node $classMethodsToRemove = []; foreach ($node->getProperties() as $property) { + $propertyName = $this->getName($property); + if ($property->isReadonly()) { continue; } - $propertyName = $this->getName($property); - $candidateClassMethods = $this->setterAndGetterFinder->findGetterAndSetterClassMethods( $node, $propertyName From 448163f8fbc74b422a7507a3fe85bdae98feac3f Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 14 Oct 2025 21:59:37 +0200 Subject: [PATCH 13/13] skip readonly promtoed property --- .../skip_readonly_promoted_property.php.inc | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_promoted_property.php.inc diff --git a/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_promoted_property.php.inc b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_promoted_property.php.inc new file mode 100644 index 00000000000..0e1c8210636 --- /dev/null +++ b/rules-tests/Php84/Rector/Class_/PropertyHookRector/Fixture/skip_readonly_promoted_property.php.inc @@ -0,0 +1,17 @@ +value; + } +}