Skip to content
Dependency Injection
step 1/7

Reading — step 1 of 7

Learn

~3 min readModern PHP 8+

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

  • new inside 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…