Dependency Injection
debt(d5/e3/b5/t5)
Closest to 'specialist tool catches it' (d5). PHPStan, Psalm, and Semgrep (from detection_hints.tools) can detect the pattern of 'new SomeService()' inside class methods/constructors, but this requires configuration and isn't caught by default IDE or basic linting. Static analysis must be specifically set up to flag internal instantiation as a code smell.
Closest to 'simple parameterised fix' (e3). The quick_fix indicates constructor injection is a straightforward replacement for internal 'new' calls. However, it's rarely a true one-liner — you need to add constructor parameters, update call sites that instantiate the class, and potentially configure a container. This typically touches a few files but stays within one component.
Closest to 'persistent productivity tax' (b5). DI is a cross-cutting architectural choice that applies to all PHP contexts (web, cli, queue-worker per applies_to). Once adopted, it shapes how every service is structured and wired. Not adopting it creates ongoing friction in testing and coupling. It's not quite 'defines the system's shape' (b9) since you can mix approaches, but it's a persistent consideration in every new class.
Closest to 'notable trap' (t5). The misconception explicitly states developers wrongly believe DI requires a container or framework. This is a documented gotcha that most devs eventually learn. Additionally, common_mistakes lists injecting the container itself (service locator anti-pattern) — a trap where doing what seems convenient is actually wrong. The concept name suggests 'injection' machinery when manual constructor passing suffices.
Also Known As
TL;DR
Explanation
Dependency injection (DI) means a class receives its collaborators (database connection, mailer, logger) as constructor arguments or method parameters rather than instantiating them internally with new. This makes the class testable (you can pass mock collaborators in tests), configurable (swap implementations without changing the class), and loosely coupled (the class depends on an interface, not a concrete implementation). Constructor injection is the preferred form — it makes dependencies explicit and the class impossible to create in an invalid state.
Diagram
flowchart LR
subgraph Without_DI
CLASS2[UserService creates its own<br/>new DatabaseConnection<br/>new EmailClient]
TIGHT[Tightly coupled<br/>untestable<br/>hardcoded deps]
end
subgraph With_DI
CONSTR[Constructor injection<br/>deps passed in]
IFACE[Depend on interface<br/>not concrete class]
CONSTR & IFACE --> LOOSE[Loosely coupled<br/>testable<br/>swappable]
end
subgraph Container
IOC[IoC Container<br/>auto-resolves dependencies]
BIND[bind interface to implementation]
IOC --> BIND
end
style TIGHT fill:#f85149,color:#fff
style LOOSE fill:#238636,color:#fff
style IOC fill:#6e40c9,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Injecting the container itself (service locator anti-pattern) — inject specific dependencies instead.
- Using constructor injection for optional dependencies — use setter injection or nullable defaults for those.
- Newing up dependencies inside methods (new Mailer()) instead of receiving them from outside.
- Registering everything as a singleton when it should be transient, causing state leakage between requests.
Code Examples
class OrderService {
public function place(Cart $cart): Order {
$mailer = new Mailer(); // hard-coded dependency
$db = new Database(); // impossible to swap/mock
$db->save($cart);
$mailer->send($cart->user, 'Your order is placed');
}
}
class OrderService {
public function __construct(private MailerInterface $mailer) {}
public function notify($order) {
$this->mailer->send($order->email, 'Order confirmed');
}
}