From 32c32a83b9f29a73582554166fe2b8f5158ef8e0 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 4 Aug 2026 10:18:23 +0200 Subject: [PATCH 1/2] [CodeQuality] Use #[Required] autowire() method instead of constructor when parent class has a constructor in ControllerMethodInjectionToConstructorRector --- ...ent_with_private_promoted_property.php.inc | 9 +- ...rent_constructor_multiple_services.php.inc | 51 +++++++ .../parent_with_autowire_method.php.inc | 47 ++++++ .../ParentControllerWithAutowireMethod.php | 21 +++ ...llerMethodInjectionToConstructorRector.php | 138 +++++++++++++++++- 5 files changed, 259 insertions(+), 7 deletions(-) create mode 100644 rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/parent_constructor_multiple_services.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/parent_with_autowire_method.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Source/ParentControllerWithAutowireMethod.php diff --git a/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/child_of_parent_with_private_promoted_property.php.inc b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/child_of_parent_with_private_promoted_property.php.inc index be8b51c2..a8f935aa 100644 --- a/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/child_of_parent_with_private_promoted_property.php.inc +++ b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/child_of_parent_with_private_promoted_property.php.inc @@ -31,14 +31,17 @@ use Symfony\Component\Routing\Annotation\Route; final class ChildOfParentWithPrivatePromotedProperty extends ParentControllerWithPrivatePromotedProperty { - public function __construct(private readonly \Psr\Log\LoggerInterface $logger) - { - } + private \Psr\Log\LoggerInterface $logger; #[Route('/example', name: 'example')] public function example() { $this->logger->log('level', 'value'); } + #[\Symfony\Contracts\Service\Attribute\Required] + public function autowire(\Psr\Log\LoggerInterface $logger): void + { + $this->logger = $logger; + } } ?> diff --git a/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/parent_constructor_multiple_services.php.inc b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/parent_constructor_multiple_services.php.inc new file mode 100644 index 00000000..de28db64 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/parent_constructor_multiple_services.php.inc @@ -0,0 +1,51 @@ +log('level', $translator->trans('value')); + } +} + +?> +----- +logger->log('level', $this->translator->trans('value')); + } + #[\Symfony\Contracts\Service\Attribute\Required] + public function autowire(\Psr\Log\LoggerInterface $logger, \Symfony\Contracts\Translation\TranslatorInterface $translator): void + { + $this->logger = $logger; + $this->translator = $translator; + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/parent_with_autowire_method.php.inc b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/parent_with_autowire_method.php.inc new file mode 100644 index 00000000..b0736d5e --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Fixture/parent_with_autowire_method.php.inc @@ -0,0 +1,47 @@ +log('level', 'value'); + } +} + +?> +----- +logger->log('level', 'value'); + } + #[\Symfony\Contracts\Service\Attribute\Required] + public function autowireServices(\Psr\Log\LoggerInterface $logger): void + { + $this->logger = $logger; + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Source/ParentControllerWithAutowireMethod.php b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Source/ParentControllerWithAutowireMethod.php new file mode 100644 index 00000000..b9681018 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector/Source/ParentControllerWithAutowireMethod.php @@ -0,0 +1,21 @@ +someService->getData(); } } +CODE_SAMPLE + ), + new CodeSample( + <<<'CODE_SAMPLE' +use Symfony\Component\Routing\Annotation\Route; + +final class SomeController extends SomeParentControllerWithConstructor +{ + #[Route('/some-path', name: 'some_name')] + public function someAction(SomeService $someService) + { + $data = $someService->getData(); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Contracts\Service\Attribute\Required; + +final class SomeController extends SomeParentControllerWithConstructor +{ + private SomeService $someService; + + #[Route('/some-path', name: 'some_name')] + public function someAction() + { + $data = $this->someService->getData(); + } + + #[Required] + public function autowire(SomeService $someService): void + { + $this->someService = $someService; + } +} CODE_SAMPLE ), ] @@ -254,9 +308,16 @@ public function refactor(Node $node): ?Node unset($methodToModify->params[$paramKey]); } - // 1. update constructor - foreach ($propertyMetadatas as $propertyMetadata) { - $this->classDependencyManipulator->addConstructorDependency($node, $propertyMetadata); + // 1. add dependencies + if ($propertyMetadatas !== []) { + if ($this->hasParentConstructor($node)) { + // constructor would have to repeat all parent params, use setter injection instead + $this->addRequiredAutowireClassMethod($node, $propertyMetadatas); + } else { + foreach ($propertyMetadatas as $propertyMetadata) { + $this->classDependencyManipulator->addConstructorDependency($node, $propertyMetadata); + } + } } foreach ($node->getMethods() as $classMethod) { @@ -390,6 +451,75 @@ private function hasAccessibleParentProperty(Class_ $class, string $propertyName return false; } + /** + * @param PropertyMetadata[] $propertyMetadatas + */ + private function addRequiredAutowireClassMethod(Class_ $class, array $propertyMetadatas): void + { + $autowireMethodName = $this->resolveAutowireMethodName($class); + + $autowireClassMethod = $class->getMethod($autowireMethodName); + $isNewClassMethod = ! $autowireClassMethod instanceof ClassMethod; + + if (! $autowireClassMethod instanceof ClassMethod) { + $autowireClassMethod = new ClassMethod(new Identifier($autowireMethodName), [ + 'flags' => Modifiers::PUBLIC, + 'returnType' => new Identifier('void'), + 'attrGroups' => [ + new AttributeGroup([new Attribute(new FullyQualified(SymfonyAttribute::REQUIRED))]), + ], + 'stmts' => [], + ]); + } + + foreach ($propertyMetadatas as $propertyMetadata) { + $propertyName = $propertyMetadata->getName(); + $propertyType = $propertyMetadata->getType(); + + $property = $this->nodeFactory->createPrivatePropertyFromNameAndType($propertyName, $propertyType); + $this->classInsertManipulator->addAsFirstMethod($class, $property); + + $autowireClassMethod->params[] = new Param( + new Variable($propertyName), + null, + $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($propertyType, TypeKind::PARAM) + ); + + $autowireClassMethod->stmts[] = new Expression( + new Assign(new PropertyFetch(new Variable('this'), $propertyName), new Variable($propertyName)) + ); + } + + if ($isNewClassMethod) { + $class->stmts[] = $autowireClassMethod; + } + } + + private function resolveAutowireMethodName(Class_ $class): string + { + $classReflection = $this->reflectionResolver->resolveClassReflection($class); + if (! $classReflection instanceof ClassReflection) { + return self::AUTOWIRE_METHOD_NAME; + } + + foreach ($classReflection->getParents() as $parentClassReflection) { + if ($parentClassReflection->hasNativeMethod(self::AUTOWIRE_METHOD_NAME)) { + return self::FALLBACK_AUTOWIRE_METHOD_NAME; + } + } + + return self::AUTOWIRE_METHOD_NAME; + } + + private function hasParentConstructor(Class_ $class): bool + { + $classReflection = $this->reflectionResolver->resolveClassReflection($class); + if (! $classReflection instanceof ClassReflection) { + return false; + } + return array_any($classReflection->getParents(), fn (ClassReflection $parentClassReflection): bool => $parentClassReflection->hasNativeMethod(MethodName::CONSTRUCT)); + } + private function isUsedInStaticClosureUse(ClassMethod $classMethod, string $paramName): bool { if ($classMethod->stmts === null) { From 58957bd03eaf5831d30aca76d73fda5ade8a18ce Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 4 Aug 2026 08:19:32 +0000 Subject: [PATCH 2/2] [rector] Rector fixes --- .../Class_/ControllerMethodInjectionToConstructorRector.php | 1 + 1 file changed, 1 insertion(+) diff --git a/rules/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector.php b/rules/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector.php index a55d9531..d45796fa 100644 --- a/rules/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector.php +++ b/rules/CodeQuality/Rector/Class_/ControllerMethodInjectionToConstructorRector.php @@ -517,6 +517,7 @@ private function hasParentConstructor(Class_ $class): bool if (! $classReflection instanceof ClassReflection) { return false; } + return array_any($classReflection->getParents(), fn (ClassReflection $parentClassReflection): bool => $parentClassReflection->hasNativeMethod(MethodName::CONSTRUCT)); }