DI Containers — PHP-DI, Symfony & Laravel Compared
debt(d7/e5/b7/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints.tools field is not specified. The primary misuse — calling the container from inside business logic (service location) — is not caught by compilers or standard linters. It requires deliberate code review to spot `app()->make()` or `$container->get()` calls inside service classes, or a custom static analysis rule. No standard tool automatically flags this pattern across all PHP DI frameworks.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix is conceptually simple (always use constructor injection), but the common_mistakes list reveals several intertwined issues: binding concretes to concretes, incorrect singleton scope decisions, missing container compilation, and circular dependencies. Remediation typically requires touching multiple service classes and their bindings across the codebase — not a single-line fix, but a pattern-level refactor across one or more components.
Closest to 'strong gravitational pull' (d7, scored as b7). The applies_to scope covers both web and cli contexts in PHP, meaning the container choice and its misuse patterns (service locator leakage, singleton scope decisions, container compilation strategy) shape how every future service, test, and bootstrap file is written. The container is load-bearing infrastructure — nearly every class definition and test setup is influenced by which container is chosen and how it is configured.
Closest to 'serious trap (contradicts how a similar concept works elsewhere)' (t7). The misconception field explicitly states the canonical wrong belief: that a DI container is the same as the Service Locator anti-pattern. A competent developer familiar with other frameworks may naturally reach for `$container->get()` inside classes because they've seen similar patterns elsewhere, not realising this couples business logic to the container. This contradicts the stated purpose of DI and is a well-documented but frequently repeated mistake.
Also Known As
TL;DR
Explanation
A DI container is a registry that maps abstract types to concrete implementations and constructs objects with their dependencies resolved. Modern PHP containers use autowiring: they inspect constructor type hints via reflection and recursively build all dependencies. PHP-DI is a standalone container with explicit configuration via PHP, PHP attributes, or YAML. Symfony's container compiles to PHP code at build time — there is no runtime reflection overhead in production. Laravel's service container uses a mix of automatic binding (any type-hinted class is auto-resolved) and explicit bindings for interfaces. All three support singletons (shared instances), factories (new instance per resolution), and lazy loading. PSR-11 defines the container interface: 'get()' and 'has()'.
Common Misconception
Why It Matters
Common Mistakes
- Binding concrete classes to concrete classes — bind interfaces to implementations; this is what enables swapping implementations for testing.
- Making everything a singleton — singletons share state across requests; only stateless services should be singletons; repositories and value-heavy objects often should not be.
- Not compiling the Symfony container in production — the development container uses lazy compilation; run 'bin/console cache:warmup' in CI to get the fast compiled version.
- Circular dependencies — Class A needs B, B needs A; the container throws; redesign using events, factories, or lazy injection.
Code Examples
<?php
// ❌ Service locator — class depends on the container
class OrderService
{
public function processOrder(int $id): void
{
// Fetching dependencies from inside the class = service locator
$repo = app()->make(OrderRepository::class);
$mailer = app()->make(Mailer::class);
$order = $repo->find($id);
$mailer->send($order->customer->email, 'Order confirmed');
}
}
<?php
// ✅ Constructor injection — container resolves at bootstrap
class OrderService
{
public function __construct(
private readonly OrderRepository $repo,
private readonly Mailer $mailer,
) {}
public function processOrder(int $id): void
{
$order = $this->repo->find($id);
$this->mailer->send($order->customer->email, 'Order confirmed');
}
}
// Laravel: automatic resolution via type hints — no binding needed
// Symfony: compiled container from services.yaml
// PHP-DI: reflection-based autowiring
// All three handle: new OrderService(new DoctrineOrderRepository(...), new SmtpMailer(...))
// You write: $container->get(OrderService::class)