Dependency Injection
Dependency Injection (DI) is the practice of passing collaborators into an object rather than letting it construct them. The win: each class becomes testable in isolation, and the wiring up of the app moves to one place. A DI container is the optional convenience; the pattern works without one.
Constructor DI, container, lifetimes, anti-patterns
EXAMPLE
<?php
// ============================================================
// 1) Constructor injection — the simplest form, no container needed
// ============================================================
interface Mailer {
public function send(string $to, string $subject, string $body): void;
}
class SmtpMailer implements Mailer {
public function __construct(private string $host, private int $port) {}
public function send(string $to, string $subject, string $body): void {
// ... open socket, send, close
}
}
class OrderService {
public function __construct(
private OrderRepository $repo,
private Mailer $mailer,
private Clock $clock,
) {}
public function placeOrder(int $customerId, array $items): int {
$order = $this->repo->create($customerId, $items, $this->clock->now());
$this->mailer->send($order->customerEmail, 'Order placed', '...');
return $order->id;
}
}
// ============================================================
// 2) Wiring — done ONCE at the composition root (entry point)
// ============================================================
$mailer = new SmtpMailer(host: 'smtp.example.com', port: 587);
$service = new OrderService(
repo: new OrderRepository($pdo),
mailer: $mailer,
clock: new SystemClock(),
);
// ============================================================
// 3) Tests — inject doubles instead of the real collaborators
// ============================================================
class FakeMailer implements Mailer {
public array $sent = [];
public function send(string $to, string $subject, string $body): void {
$this->sent[] = compact('to', 'subject', 'body');
}
}
class FixedClock implements Clock {
public function __construct(private DateTimeImmutable $now) {}
public function now(): DateTimeImmutable { return $this->now; }
}
$service = new OrderService(
repo: new InMemoryOrderRepository(),
mailer: $fake = new FakeMailer(),
clock: new FixedClock(new DateTimeImmutable('2026-06-18 09:00:00')),
);
$service->placeOrder(42, [/*...*/]);
assert(count($fake->sent) === 1);
// ============================================================
// 4) Service container (Laravel, Symfony, etc.) — sugar on top
// ============================================================
// Laravel example
// app()->singleton(Clock::class, SystemClock::class);
// app()->bind(Mailer::class, fn($c) => new SmtpMailer('smtp.example.com', 587));
//
// Then constructor type-hints are resolved automatically:
// public function __construct(private OrderRepository $repo, private Mailer $mailer, private Clock $clock) {}
//
// The container picks the right concrete based on the binding.
// ============================================================
// 5) Lifetimes — choose deliberately
// ============================================================
// Singleton: constructed once per process (logger, clock, config)
// Scoped: one per HTTP request (current_user, request id)
// Transient: a fresh instance every time (use cases, jobs)
// Most app objects are transient; singletons creep into hard-to-test corners.
// ============================================================
// 6) Anti-patterns
// ============================================================
// a) Service locator hidden inside a class
// class OrderService { public function place() { $mailer = app(Mailer::class); ... } }
// The dependency is invisible to the constructor + test setup. Avoid.
//
// b) 'new' inside the class
// class OrderService { public function send() { (new SmtpMailer)->send(...); } }
// You cannot replace SmtpMailer in tests. Pass it in.
//
// c) Container 'features' that magic-load anything
// Most apps need 5-15 explicit bindings. Stop reading frameworks for tricks
// and start writing the bindings in a single, readable file.
//
// d) Constructor with 10 dependencies
// Smell: the class is doing too much. Split it.
// ============================================================
// 7) Without a framework — a tiny container is fine
// ============================================================
class Container {
private array $bindings = [];
public function bind(string $abstract, callable $concrete): void {
$this->bindings[$abstract] = $concrete;
}
public function make(string $abstract) {
$factory = $this->bindings[$abstract] ?? null;
if (!$factory) throw new RuntimeException('not bound: '.$abstract);
return $factory($this);
}
}
$c = new Container();
$c->bind(Clock::class, fn() => new SystemClock());
$c->bind(Mailer::class, fn() => new SmtpMailer('smtp.example.com', 587));
// ... and so on
$service = new OrderService(
repo: $c->make(OrderRepository::class),
mailer: $c->make(Mailer::class),
clock: $c->make(Clock::class),
);
interface Clock { public function now(): DateTimeImmutable; }
class SystemClock implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable(); } }
class OrderRepository { public function __construct($pdo){} public function create($c,$i,$t){return (object)['id'=>1,'customerEmail'=>'a@b']; } }
class InMemoryOrderRepository extends OrderRepository { public function __construct(){} }
Why it matters
Constructor injection + a tiny composition root beats a magical container in 80% of codebases. You can see every dependency at the top of each class and every wiring in one file — refactors become find-and-replace, and tests stop fighting the framework to swap a collaborator. Reach for a framework container only when you have grown past the point where wiring by hand is a chore.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Pass deps in; don't construct them inside.
class UserService {
constructor(repo, mailer) { this.repo = repo; this.mailer = mailer; }
create(data) { const u = this.repo.add(data); this.mailer.welcome(u); return u; }
}
Try it Yourself »
Exercise
Common DI shape in a class…
constructor(repo,
) { /* keep deps */ }
Six letters.
Discussion
Loading…