Reading — step 1 of 7
Learn
Dependency Injection (DI) is the dominant pattern in modern PHP frameworks (Laravel, Symfony, Slim). It's the practice of passing dependencies INTO a class rather than letting the class create them — making code testable, swappable, and decoupled.
The problem DI solves
<?php
// BAD — UserService is tightly coupled to Database and Mailer
class UserService {
private Database $db;
private Mailer $mailer;
public function __construct() {
$this->db = new Database('localhost');
$this->mailer = new SmtpMailer('smtp.example.com');
}
public function register(string $email): void {
$id = $this->db->insertUser($email);
$this->mailer->send($email, 'welcome');
}
}
Problems:
- Can't test in isolation — every test hits the real DB
- Can't swap Mailer (e.g., for a test fake)
- The hardcoded connection strings are buried inside
Constructor injection — the fix
class UserService {
public function __construct(
private Database $db,
private Mailer $mailer,
) {}
public function register(string $email): void {
$id = $this->db->insertUser($email);
$this->mailer->send($email, 'welcome');
}
}
// Caller wires up:
$service = new UserService(
new Database('localhost'),
new SmtpMailer('smtp.example.com')
);
Now:
- Tests can pass mock implementations
- Different environments (test, prod) wire different concrete classes
- The class signature documents what it needs
Programming to interfaces
Better yet, depend on INTERFACES, not concrete classes:
interface Mailer {
public function send(string $to, string $template): void;
}
class SmtpMailer implements Mailer { /* ... */ }
class LogMailer implements Mailer { /* writes to log instead */ }
class NullMailer implements Mailer { /* no-op for tests */ }
class UserService {
public function __construct(
private Database $db,
private Mailer $mailer, // any Mailer
) {}
}
Now tests can pass a NullMailer, dev can pass a LogMailer, prod uses SmtpMailer. Same UserService, different implementations. The Liskov-Substitution Principle in action.
DI containers
For large apps, manually wiring dependencies becomes tedious. DI containers automate it:
$container = new Container();
$container->bind(Mailer::class, SmtpMailer::class);
$container->bind(Database::class, fn() => new Database(getenv('DB_HOST')));
$service = $container->make(UserService::class);
// container inspects UserService's constructor, resolves Database and Mailer,
// instantiates them, then UserService
Real containers (Laravel's, Symfony's, PHP-DI) use Reflection under the hood — exactly what you'd write yourself with ReflectionClass::getConstructor()->getParameters().
Constructor vs property vs setter injection
- Constructor — preferred. Required dependencies; class always valid; immutable.
- Setter — for OPTIONAL dependencies set after construction. Less common.
- Property — frameworks sometimes inject directly into properties (especially with attributes). Hidden contracts; harder to test. Use sparingly.
When NOT to use DI
- Pure value objects (Money, Email, Coordinate) — no dependencies, just data. DI would be over-engineering.
- Simple scripts — for a 30-line script, the boilerplate isn't worth it.
- Static helpers (
Math::sqrt) — stateless, no dependencies.
DI shines when you have NETWORKED, EXTERNAL, or POLYMORPHIC dependencies. App services, repositories, controllers — yes. Plain data objects — no.
PSR-11: ContainerInterface
The PHP-FIG standard for containers. Frameworks comply so libraries can work across containers:
use Psr\Container\ContainerInterface;
interface ContainerInterface {
public function get(string $id): mixed;
public function has(string $id): bool;
}
Write to this interface and your library works with Laravel, Symfony, PHP-DI, etc. Standard part of professional PHP work.
Common mistakes
newinside a service — defeats DI. Pass it in.- Service Locator pattern (passing the container around to fetch deps) — looks like DI but isn't. Tightly couples to the container.
- Too many dependencies in one class — if the constructor takes 10 args, the class does too much. Split it.
- Singletons via DI that have mutable state — leaks state between requests. Use carefully.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…