The best way to pass dependencies is via constructor injection. Only in services, we need the dependency in. I've noticed that sometimes the dependency is passed way too early, just to be passed to another service as a method argument.
Why is it a code smell, and how can we avoid it?
Because constructor injection has good karma, we automatically assume that everything in __construct(...)
is correct. That's why we can easily miss it.
Imagine this code in your project:
final class TypeResolver
{
public function __construct(
// using PHP 8 property promotion here
private StringTypeResolver $stringTypeResolver,
private ConstantStringTypeResolver $constantStringTypeResolver,
) {
}
public function resolve(Expr $expr): Type
{
return $this->stringTypeResolver->resolveFromExpr(
$expr,
$this->constantStringTypeResolver,
);
}
}
Can you see the smell in here? We have 2 services passed as a dependency in the constructor:
StringTypeResolver
ConstantStringTypeResolver
Constructor states what dependencies this class needs to work.
How many of them the class directly uses? Just the StringTypeResolver
one.
The other one ConstantStringTypeResolver
is juggled to StringTypeResolver
as side dependency.
It's like your own keys from your home with one extra pair. You take them both with you everywhere. You use the first pair to unlock doors from your home. The extra pair you use exclusively when you open your door for your wife.
Why would you have these extra keys for your own? Instead, give them to your wife and let her handle it.
Each service should take care only of its own dependencies.
not take responsibility for others.
If we apply this approach to our code, we can move the ConstantStringTypeResolver
where it belongs:
final class TypeResolver
{
public function __construct(
private StringTypeResolver $stringTypeResolver,
- private ConstantStringTypeResolver $constantStringTypeResolver,
) {
}
public function resolve(Expr $expr): Type
{
return $this->stringTypeResolver->resolveFromExpr(
$expr,
- $this->constantStringTypeResolver,
);
}
}
The StringTypeResolver
now handles its dependencies on its own:
final class StringTypeResolver
{
+ public function __construct()
+ {
+ private ConstantStringTypeResolver $constantStringTypeResolver,
+ }
public function resolveFromExpr(
Expr $expr,
- ConstantStringTypeResolver $constantStringTypeResolver,
) {
- $constantStringTypeResolver->resolve(...);
+ $this->constantStringTypeResolver->resolve(...);
// ...
}
}
👍
The tricky part is to watch for these cases not to get into your code. It's easy to take 2 key sets instead of 1 when you're in a rush to catch a tram or taxi.
Symplify PHPStan Rules got you covered. Just install it:
composer require symplify/phpstan-rules --dev
Then add the rule to your phpstan.neon
config:
rules:
- Symplify\PHPStanRules\Rules\NoDependencyJugglingRule
The rule looks for constructor dependencies that are juggled to method calls. Don't worry about it, and let your CI handle it. That's all for today.
Happy coding!
Do you learn from my contents or use open-source packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!