Dependency Injection in Modern PHP
Dependency injection means an object receives the collaborators it needs instead of locating or constructing them. That small constraint produces clearer APIs and cheaper tests.
Dependencies are part of the type’s contract
A constructor tells readers what must exist for the object to work. Depending on interfaces separates policy from implementation and allows infrastructure to change without rewriting business rules.
Prefer constructor injection
Constructor injection creates valid objects in one step. Setter injection is appropriate only for genuinely optional behavior; service location hides requirements and moves failures to runtime.
Testing gets smaller
A service with explicit collaborators can be tested with a fake clock, repository, or mailer. The test focuses on decisions rather than booting the framework.
Working example
final class PublishArticle {
public function __construct(
private ArticleRepository $articles,
private Clock $clock,
) {}
public function __invoke(Article $article): void {
$article->publishAt($this->clock->now());
$this->articles->save($article);
}
}Dependency injection in Drupal
Drupal's service container is where many framework dependencies come from: entity storage, configuration, logging, HTTP clients, time services, language services, and custom application services. A class that receives those collaborators through its constructor documents what it needs in a way both Drupal and a test can satisfy.
Controllers and many plugins are created by the container through factory methods. The factory is framework glue; the constructor remains the actual contract of the object. This keeps service lookup at the boundary instead of spreading calls to the global container through application logic.
Do not inject the whole container
Passing the complete container into a class technically provides every service, but it recreates service location under another name. The class can acquire hidden dependencies without changing its constructor, and tests need to reproduce a container instead of supplying a few collaborators.
Inject the narrow service or interface the class actually needs. If the constructor becomes very large, that can be a useful signal that the class has accumulated more than one responsibility.
Interfaces are most useful at real boundaries
Not every private helper requires an interface. Interfaces earn their value when multiple implementations are reasonable, infrastructure may change, or tests benefit from substituting a collaborator. Good dependency injection makes change boundaries visible; it should not create abstraction for its own sake.
Key Takeaways
- Make required collaborators constructor arguments.
- Depend on narrow interfaces at architectural boundaries.
- Use injection to simplify decisions and tests, not as ceremony.