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.
I'm a Symfony trainer, and I'm told to teach people how to use Symfony and talk about this injection pattern. Sob.
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.
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);
// ...
}
}
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
Request
objects
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.
composer install rector/rector --dev
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');
};
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!