Skip to content

[CodeQuality] Use #[Required] autowire() instead of constructor when parent has one in ControllerMethodInjectionToConstructorRector - #1006

Merged
TomasVotruba merged 2 commits into
mainfrom
required-autowire-injection
Aug 4, 2026
Merged

[CodeQuality] Use #[Required] autowire() instead of constructor when parent has one in ControllerMethodInjectionToConstructorRector#1006
TomasVotruba merged 2 commits into
mainfrom
required-autowire-injection

Conversation

@TomasVotruba

Copy link
Copy Markdown
Member

When a controller extends a parent that already has a constructor, adding a promoted constructor dependency forces the child to repeat every parent parameter and call parent::__construct() with all of them:

final class SomeController extends AbstractMauticController
{
    /**
     * @param ModelFactory<object> $modelFactory
     */
    public function __construct(
        protected ManagerRegistry $doctrine,
        protected ModelFactory $modelFactory,
        UserHelper $userHelper,
        protected CoreParametersHelper $coreParametersHelper,
        protected EventDispatcherInterface $dispatcher,
        protected Translator $translator,
        private FlashBag $flashBag,
        private RequestStack $requestStack,
        protected CorePermissions $security,
        private readonly PathsHelper $pathsHelper,
        private readonly FileUploader $fileUploader,
    ) {
        parent::__construct($doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
    }
}

This PR makes the rule use #[Required] setter injection in that case instead, so the parent constructor is left alone.

Before / after

Parent class has a constructor:

 final class ChildOfParentWithPrivatePromotedProperty extends ParentControllerWithPrivatePromotedProperty
 {
+    private \Psr\Log\LoggerInterface $logger;
+
     #[Route('/example', name: 'example')]
-    public function example(LoggerInterface $logger)
+    public function example()
     {
-        $logger->log('level', 'value');
+        $this->logger->log('level', 'value');
+    }
+
+    #[\Symfony\Contracts\Service\Attribute\Required]
+    public function autowire(\Psr\Log\LoggerInterface $logger): void
+    {
+        $this->logger = $logger;
     }
 }

No parent constructor — unchanged, still promoted constructor:

 final class SomeControllerWithMethodInjection extends AbstractController
 {
+    public function __construct(private readonly \Psr\Log\LoggerInterface $logger)
+    {
+    }
+
     #[Route('/some-action', name: 'some_action')]
     public function someAction(
         \Symfony\Component\HttpFoundation\Request $request,
-        \Psr\Log\LoggerInterface $logger
     ) {
-        $logger->log('level', 'value');
+        $this->logger->log('level', 'value');
     }
 }

Notes

  • All services moved out of action methods land in a single autowire() method, one property assignment each.
  • If a parent class already defines autowire(), the generated method is named autowireServices() instead, so parent injection is not silently overridden.
  • Re-running the rule appends to the existing autowire() method rather than creating a second one.

Previously the child got its own __construct() with no parent::__construct() call at all, which broke the parent's dependencies at runtime — see the updated child_of_parent_with_private_promoted_property.php.inc fixture.

TomasVotruba and others added 2 commits August 4, 2026 10:18
…r when parent class has a constructor in ControllerMethodInjectionToConstructorRector
@TomasVotruba
TomasVotruba merged commit 283efb6 into main Aug 4, 2026
7 checks passed
@TomasVotruba
TomasVotruba deleted the required-autowire-injection branch August 4, 2026 08:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants