Skip to content

PHP Dependency Injection — Complete Guide to DI Container and Loose Coupling

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about PHP Dependency Injection. We cover key concepts, practical examples, and best practices to help you master this topic.

Dependency injection in PHP passes dependencies into a class rather than creating them internally, promoting loose coupling, testability, and maintainable object-oriented code.

What You'll Learn

By the end of this tutorial, you'll implement constructor injection, setter injection, use auto-wiring with PHP-DI, configure service containers, and write testable code with mocked dependencies.

Why DI Matters

When a class creates its own dependencies (e.g., new Database()), it's tightly coupled to that implementation. DI lets you swap implementations without changing the class, making testing and maintenance easier.

Real-World Use

A UserController receives a UserRepositoryInterface in its constructor. In production, a MySQLUserRepository is injected. In tests, a MockUserRepository is injected. The controller never knows the difference.

DI Learning Path

flowchart LR
  A[Composer] --> B[DI]
  B --> C[MVC]
  C --> D[REST API]
  D --> E[JWT Auth]
  A --> F{You Are Here}
  style F fill:#f90,color:#fff

Without DI (Tight Coupling)

<?php
class UserController {
    private Database $db;
    public function __construct() {
        $this->db = new Database("localhost", "root", "", "mydb");
    }
    public function show(int $id): array {
        return $this->db->query("SELECT * FROM users WHERE id = ?", [$id]);
    }
}
// Hard to test — always connects to real database

Constructor Injection

<?php
interface UserRepositoryInterface {
    public function find(int $id): ?array;
}
class MySQLUserRepository implements UserRepositoryInterface {
    public function find(int $id): ?array {
        return ["id" => $id, "name" => "Alice"];
    }
}
class UserController {
    public function __construct(
        private UserRepositoryInterface $repository
    ) {}
    public function show(int $id): ?array {
        return $this->repository->find($id);
    }
}
// Usage
$controller = new UserController(new MySQLUserRepository());

Setter Injection

<?php
class MailerService {
    private ?LoggerInterface $logger = null;
    public function setLogger(LoggerInterface $logger): void {
        $this->logger = $logger;
    }
    public function send(string $to, string $subject): void {
        $this->logger?->info("Sending email to $to");
        // Send email...
    }
}

Autoloading with PHP-DI

composer require php-di/php-di
<?php
require "vendor/autoload.php";
use function DI\create;
use function DI\get;
$container = new DI\Container();
$container->set(UserRepositoryInterface::class, DI\autowire(MySQLUserRepository::class));
$controller = $container->get(UserController::class);
$user = $controller->show(1);

Laravel Service Container

<?php
// Binding interfaces
app()->bind(UserRepositoryInterface::class, MySQLUserRepository::class);
// Singleton
app()->singleton(Logger::class, fn() => new Logger("app"));
// Resolving
$repo = app(UserRepositoryInterface::class);
$controller = app(UserController::class);

Common Mistakes

1. Using new Inside Classes

Creating dependencies with new inside a class couples it to concrete implementations. Inject dependencies instead.

2. Service Locator Anti-Pattern

Calling a static service locator from within a class hides dependencies. Explicit injection is better.

3. Container as Service Locator

Passing the entire container to a class makes dependencies invisible. Only inject what's needed.

4. Too Many Constructor Parameters

More than 3-4 constructor parameters suggests the class has too many responsibilities. Split it up.

5. Not Using Interfaces

Depend on interfaces, not concrete classes. This allows swapping implementations (real vs mock).

Practice Questions

1. What is dependency injection?

Passing dependencies into a class from outside rather than creating them inside. This decouples the class from its dependencies.

2. What is the difference between constructor and setter injection?

Constructor injection provides dependencies at creation time (required dependencies). Setter injection adds optional dependencies after creation.

3. What is a DI container?

A container that automatically resolves and injects dependencies based on type hints, reducing manual wiring.

4. How does DI improve testability?

Tests can inject mock implementations of interfaces, isolating the class under test from real infrastructure.

5. Challenge: Refactor a tightly coupled class to use dependency injection.

<?php
// Before: Tight coupling
class OrderService {
    public function process(Order $order): void {
        $db = new Database();
        $mailer = new Mailer();
        $db->save($order);
        $mailer->send($order->getEmail(), "Order confirmed");
    }
}
// After: Dependency injection
class OrderService {
    public function __construct(
        private OrderRepositoryInterface $db,
        private MailerInterface $mailer
    ) {}
    public function process(Order $order): void {
        $this->db->save($order);
        $this->mailer->send($order->getEmail(), "Order confirmed");
    }
}

FAQ

What is auto-wiring?

The container automatically resolves dependencies by inspecting type hints in constructors and recursively resolving each dependency.

What is the difference between DI and IoC?

DI is a technique (injecting dependencies). IoC (Inversion of Control) is the broader principle where the framework controls the flow.

Is it okay to use new for value objects?

Yes. Value objects (Money, Email, Address) have no external dependencies and can be created freely.

What is a service container?

A registry of bindings that maps interfaces to implementations and resolves dependency chains automatically.

How do I choose between PHP-DI and Pimple?

PHP-DI supports auto-wiring and annotations. Pimple is simpler (array-like). Use PHP-DI for large apps, Pimple for small ones.

Mini Project: DI Container Setup

Create a simple DI container with auto-wiring support.

<?php
class SimpleContainer {
    private array $bindings = [];
    public function set(string $abstract, callable $factory): void {
        $this->bindings[$abstract] = $factory;
    }
    public function get(string $class): object {
        if (isset($this->bindings[$class])) {
            return ($this->bindings[$class])($this);
        }
        return $this->autowire($class);
    }
    private function autowire(string $class): object {
        $reflection = new ReflectionClass($class);
        $constructor = $reflection->getConstructor();
        if (!$constructor) return $reflection->newInstance();
        $params = array_map(fn($p) => $this->get($p->getType()->getName()), $constructor->getParameters());
        return $reflection->newInstanceArgs($params);
    }
}
// Usage
$container = new SimpleContainer();
$repo = new MySQLUserRepository();
$container->set(UserRepositoryInterface::class, fn() => $repo);
$controller = $container->get(UserController::class);

What's Next

PHP MVC Pattern PHP REST API PHP PDO

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro