Service Locator Anti-Pattern
debt(d5/e5/b6/t7)
Closest to 'specialist tool catches' (d5) — phpstan/deptrac/semgrep rules can flag app()->make() or Container calls inside domain code, but this requires custom rules and architectural boundaries configuration, not default linting.
Closest to 'touches multiple files / significant refactor in one component' (e5) — quick_fix says replace with constructor injection, but doing so cascades: callers must now provide the dependency, requiring changes to factories, bindings, and tests across the component.
Closest to 'strong gravitational pull' (b7) minus one (b6) — applies_to spans web/cli/queue-worker so reach is wide; once domain classes lean on the locator, every new feature follows the same hidden-dependency shape, but it's not quite system-defining since it can be peeled back class-by-class.
Closest to 'serious trap' (t7) — the misconception explicitly states devs equate service locator with DI; it contradicts how DI is supposed to work (explicit signature) by hiding deps in implementation, fooling competent developers who think they're 'doing DI' via app().
Also Known As
TL;DR
Explanation
A service locator provides a static or global registry that classes query to obtain their dependencies. Unlike dependency injection (where dependencies are declared in the constructor), service locators make dependencies implicit — you cannot tell what a class needs without reading its implementation. This makes testing harder (must configure the global registry), increases coupling to the locator itself, and makes dependency graphs opaque. It is listed as an anti-pattern by most DI literature.
Common Misconception
Why It Matters
Common Mistakes
- Using Laravel's app() helper inside domain classes — decouples from HTTP but couples to the framework container.
- Not distinguishing service locator (anti-pattern in domain code) from the DI container itself (legitimate infrastructure).
- Passing the container into classes as a constructor argument — this is a service locator, not DI.
- Testing classes that use service locators by configuring the global container — fragile, order-dependent tests.
Code Examples
// Service locator — hidden dependencies:
class OrderService {
public function place(Order $order): void {
// Dependencies hidden inside — impossible to see from the outside:
$mailer = app(Mailer::class);
$payment = app(PaymentGateway::class);
$logger = app(Logger::class);
// Test must configure global app() — hidden coupling
}
}
// Dependency injection — explicit dependencies:
class OrderService {
public function __construct(
private readonly Mailer $mailer,
private readonly PaymentGateway $payment,
private readonly LoggerInterface $logger,
) {} // All dependencies visible in signature — trivial to test
public function place(Order $order): void {
// Uses injected dependencies
}
}