How to Instantly Refactor Symfony Action Injects to Constructor Injection

Action Injections are much fun a first, but they turn your fresh project into legacy code very fast. With PHP 8 and promoted properties, there is no reason to pollute method arguments with services.

How to refactor out of the legacy back to constructor injection today?

Action Injection or Method Injection is Laravel and Symfony feature, that turns Controller action method to injected constructor:

final class SomeController
{
    public function actionDetail(int $id, User $user, PostRepository $postRepository)
    {
        $post = $postRepository->get($id);
        if (! $user->hasAccess($post)) {
            // ...
        }

        // ...
    }
}

It looks sexy and fun at first, but in few months, it will reveal its true face as an ugly code smell:


Action injection makes it confusing whether an object is treated stateful or stateless - a very grey area with, e.g., the Session.
Iltar van der Berg
I'm a Symfony trainer, and I'm told to teach people how to use Symfony and talk about this injection pattern. Sob.
Alex Rock
I work on a project that uses action injection, and I hate it. The whole idea about action injection is broken. Development with this pattern is a total nightmare.
A

It's natural to try new patterns with an open heart and validate them in practice, but what if you find this way as not ideal and want to go to constructor injection instead?

How would you change all your 50 controllers with action injections...

final class SomeController
{
    public function detail(int $id, Request $request, ProductRepository $productRepository)
    {
        $this->validateRequest($request);
        $product = $productRepository->find($id);
        // ...
    }
}

...to the constructor injection:

final class SomeController
{
    public function __construct(
        private ProductRepository $productRepository
    ) {
    }

    public function detail(int $id, Request $request)
    {
        $this->validateRequest($request);
        $product = $this->productRepository->find($id);
        // ...
    }
}

How to Waste a Week in one Team?

Let's say your project is fairly small, e.g. 50 controllers, there are four action methods per each. So you have to refactor 200 service arguments to constructor injection. You decided to do it manually.

  • some of the services them are duplicated

  • you have to identify 3 parts

  • there will be code-reviews and discussions that might take up to 5-10 days

  • and of course, rebase on new merged PRs... you have another 4-10 hours of team-work wasted ahead of you


We find the time of our client's team very precious, don't you? So we Let Rector do the work.

3 Steps to Instant Refactoring

1. Install Rector

composer install rector/rector --dev

2. Prepare Config

Enable the set and configure your Kernel class name in rector.php config:

use Rector\Set\ValueObject\SetList;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->import(SetList::ACTION_INJECTION_TO_CONSTRUCTOR_INJECTION);

    // the default value
    $parameters = $containerConfigurator->parameters();
    $parameters->set('kernel_class', 'App\Kernel');
};

3. Run Rector on Your Code

vendor/bin/rector process /app

You will see diffs like:

 final class SomeController
 {
+    public function __construct(
+         private ProductRepository $productRepository
+    ) {
+    }
+
-    public function detail(int $id, Request $request, ProductRepository $productRepository)
+    public function detail(int $id, Request $request)
     {
         $this->validateRequest($request);
-        $product = $productRepository->find($id);
+        $product = $this->productRepository->find($id);
         // ...
     }
 }

And your code is now both refactored and clean. That's it!



Happy instant refactoring!