Skip to content

PHP Dependency Injection — Constructor Injection, Autowiring, and Container Patterns

DodaTech Updated 2026-06-28 8 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.

PHP dependency injection is a design pattern where objects receive their dependencies from an external source rather than creating them internally, enabling loose coupling, testability, and configurable application architecture.

Why It Matters

Hard-coded dependencies make code rigid and impossible to test in isolation. If a UserService creates a DatabaseConnection internally, you cannot test it without a real database. Dependency injection lets you pass in a mock database for testing and a real one for production. Frameworks like Laravel and Symfony use dependency injection containers that automatically resolve and inject dependencies, drastically reducing boilerplate code.

Real-World Use

Laravel's service container resolves dependencies recursively, autowiring constructor parameters. Symfony's dependency injection container uses configuration files to define service relationships. PHPUnit uses test doubles (mocks) injected into classes under test. DodaTech's PHP services use constructor injection with a PSR-11 container for mail services, database repositories, and logging.

What You Will Learn

  • What dependency injection is and why it matters
  • Constructor injection vs setter injection
  • Building a simple dependency injection container
  • Autowiring with reflection
  • Service definition and configuration
  • PSR-11 Container Interface

Learning Path

flowchart LR
  A[MVC Pattern] --> B[Dependency Injection
You are here] B --> C[Testing] C --> D[Debugging] D --> E[Composer] style B fill:#f90,color:#fff

The Problem: Tight Coupling

Without dependency injection, classes create their own dependencies:

<?php
class UserService {
    private DatabaseConnection $db;

    public function __construct() {
        // Tight coupling: UserService depends on DatabaseConnection concretely
        $this->db = new DatabaseConnection('localhost', 'root', '');
    }

    public function findByEmail(string $email): ?array {
        return $this->db->query("SELECT * FROM users WHERE email = ?", [$email]);
    }
}

// Cannot test without a real database!
// Cannot switch to PostgreSQL without changing UserService!

The Solution: Constructor Injection

Pass dependencies through the constructor:

<?php
interface DatabaseInterface {
    public function query(string $sql, array $params = []): array;
}

class MySQLDatabase implements DatabaseInterface {
    public function query(string $sql, array $params = []): array {
        // MySQL implementation
        return [];
    }
}

class PostgresDatabase implements DatabaseInterface {
    public function query(string $sql, array $params = []): array {
        // PostgreSQL implementation
        return [];
    }
}

class UserService {
    public function __construct(
        private readonly DatabaseInterface $db,  // Interface, not concrete class
    ) {}

    public function findByEmail(string $email): ?array {
        return $this->db->query("SELECT * FROM users WHERE email = ?", [$email]);
    }
}

// Usage in production
$service = new UserService(new MySQLDatabase());

// Usage in tests
$mockDb = $this->createMock(DatabaseInterface::class);
$service = new UserService($mockDb);  // Testable!

Setter Injection

Alternative to constructor injection, useful for optional dependencies:

<?php
class Logger {
    private ?string $logFile = null;
    private ?array $config = null;

    // Required dependencies via constructor
    public function __construct(
        private readonly string $channel,
    ) {}

    // Optional dependencies via setters
    public function setLogFile(string $path): void {
        $this->logFile = $path;
    }

    public function setConfig(array $config): void {
        $this->config = $config;
    }

    public function log(string $message): void {
        $prefix = '[' . date('Y-m-d H:i:s') . '] [' . $this->channel . '] ';
        $full = $prefix . $message;

        if ($this->logFile) {
            file_put_contents($this->logFile, $full . "\n", FILE_APPEND);
        } else {
            echo $full . "\n";
        }
    }
}

// Usage
$logger = new Logger('app');
$logger->setLogFile('/var/log/app.log');  // Optional configuration

Building a Simple DI Container

A container stores service definitions and resolves dependencies:

<?php
namespace App\Container;

class Container {
    private array $bindings = [];
    private array $instances = [];

    // Bind an interface to a concrete class or factory
    public function bind(string $abstract, callable|string $concrete): void {
        $this->bindings[$abstract] = $concrete;
    }

    // Bind a singleton (same instance every time)
    public function singleton(string $abstract, callable|string $concrete): void {
        $this->bindings[$abstract] = $concrete;
        $this->instances[$abstract] = null;  // Mark as singleton
    }

    // Resolve a dependency
    public function make(string $abstract): mixed {
        // Return existing singleton instance
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        // Resolve from binding
        if (isset($this->bindings[$abstract])) {
            $concrete = $this->bindings[$abstract];

            if (is_callable($concrete)) {
                $instance = $concrete($this);
            } else {
                $instance = $this->resolve($concrete);
            }
        } else {
            // Autowire if no binding exists
            $instance = $this->resolve($abstract);
        }

        // Store singleton
        if (array_key_exists($abstract, $this->instances ?? [])) {
            $this->instances[$abstract] = $instance;
        }

        return $instance;
    }

    // Autowire: resolve constructor parameters using reflection
    private function resolve(string $class): object {
        $reflection = new \ReflectionClass($class);

        if (!$reflection->isInstantiable()) {
            throw new \RuntimeException("Class $class is not instantiable");
        }

        $constructor = $reflection->getConstructor();
        if (!$constructor) {
            return $reflection->newInstance();
        }

        $parameters = $constructor->getParameters();
        $dependencies = [];

        foreach ($parameters as $parameter) {
            $type = $parameter->getType();

            if ($type === null || $type->isBuiltin()) {
                if ($parameter->isDefaultValueAvailable()) {
                    $dependencies[] = $parameter->getDefaultValue();
                } else {
                    throw new \RuntimeException(
                        "Cannot resolve parameter {$parameter->getName()} in $class"
                    );
                }
            } else {
                $dependencies[] = $this->make($type->getName());
            }
        }

        return $reflection->newInstanceArgs($dependencies);
    }
}

Using the Container

<?php
// Define interfaces and implementations
interface MailerInterface {
    public function send(string $to, string $subject, string $body): bool;
}

class SmtpMailer implements MailerInterface {
    public function send(string $to, string $subject, string $body): bool {
        echo "Sending email to $to: $subject\n";
        return true;
    }
}

class NotificationService {
    public function __construct(
        private readonly MailerInterface $mailer,
        private readonly Logger $logger,
    ) {}

    public function notifyUser(string $email, string $message): void {
        $this->mailer->send($email, 'Notification', $message);
        $this->logger->log("Notified $email");
    }
}

// Configure container
$container = new Container();

$container->bind(MailerInterface::class, SmtpMailer::class);
$container->singleton(Logger::class, function ($c) {
    return new Logger('app');
});

// Resolve and use
$notification = $container->make(NotificationService::class);
$notification->notifyUser('alice@example.com', 'Your order has been shipped.');

PSR-11 Container Interface

The PHP-FIG standard for containers:

<?php
namespace Psr\Container;

interface ContainerInterface {
    public function get(string $id): mixed;
    public function has(string $id): bool;
}

interface ContainerExceptionInterface extends \Throwable {}
interface NotFoundExceptionInterface extends ContainerExceptionInterface {}

Implement PSR-11 for interoperability with frameworks:

<?php
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

class Psr11Container implements ContainerInterface {
    public function __construct(
        private readonly Container $container,
    ) {}

    public function get(string $id): mixed {
        if (!$this->has($id)) {
            throw new class("Service $id not found") extends \RuntimeException
                implements NotFoundExceptionInterface {};
        }
        return $this->container->make($id);
    }

    public function has(string $id): bool {
        try {
            $this->container->make($id);
            return true;
        } catch (\Throwable) {
            return false;
        }
    }
}

Service Providers

Organize container configuration into service providers:

<?php
interface ServiceProviderInterface {
    public function register(Container $container): void;
    public function boot(): void;
}

class DatabaseServiceProvider implements ServiceProviderInterface {
    public function register(Container $container): void {
        $container->singleton(DatabaseInterface::class, function ($c) {
            return new MySQLDatabase(
                $_ENV['DB_HOST'],
                $_ENV['DB_NAME'],
                $_ENV['DB_USER'],
                $_ENV['DB_PASSWORD'],
            );
        });
    }

    public function boot(): void {
        // Run migrations, verify connection, etc.
    }
}

class AppServiceProvider implements ServiceProviderInterface {
    public function register(Container $container): void {
        $container->bind(UserRepository::class, UserRepository::class);
        $container->bind(UserService::class, UserService::class);
    }

    public function boot(): void {
        // No boot logic needed
    }
}

Common Mistakes

1. Service Locator Pattern

class UserController {
    public function show(int $id) {
        $container = App::getContainer();  // Service locator!
        $service = $container->get(UserService::class);
    }
}

This hides dependencies. Inject them explicitly through the constructor.

2. Overusing Setter Injection

Constructor injection makes dependencies explicit and mandatory. Use setters only for truly optional dependencies.

3. Not Using Interfaces

$container->bind(UserService::class, UserService::class);  // Works but inflexible

Bind to interfaces, not concrete classes. This lets you swap implementations.

4. Fat Container Configuration

When your container configuration has hundreds of lines, consider service providers that group related bindings.

5. Circular Dependencies

If A depends on B and B depends on A, the autowirer enters infinite Recursion. Detect and break circular dependencies by redesigning classes.

Practice Questions

  1. What is the difference between a service container and dependency injection? Dependency injection is the pattern of passing dependencies externally. A service container is a tool that resolves and injects dependencies automatically.

  2. Why prefer constructor injection over setter injection? Constructor injection makes dependencies explicit and mandatory. Objects are fully initialized after construction. Setters allow partial object state.

  3. What is autowiring? Autowiring uses reflection to inspect constructor parameters and automatically resolve them from the container. It reduces manual configuration.

  4. What is the difference between bind and Singleton in a container? bind creates a new instance each time. singleton returns the same instance. Use singleton for stateless services like loggers and database connections.

  5. Challenge: Build a container with autowiring, singleton support, and service providers, then wire up a complete application with database, mailer, and logging.

Mini Project: Application Wiring

Wire an application using dependency injection:

<?php
// config/services.php
use App\Container\Container;
use App\Services\MailerInterface;
use App\Services\SmtpMailer;
use App\Repositories\UserRepository;
use App\Services\UserService;
use App\Controllers\UserController;

$container = new Container();

// Bind interfaces to implementations
$container->singleton(DatabaseInterface::class, function ($c) {
    return new MySQLDatabase($_ENV['DB_HOST'], $_ENV['DB_NAME'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD']);
});

$container->singleton(MailerInterface::class, SmtpMailer::class);
$container->singleton(UserRepository::class, function ($c) {
    return new UserRepository($c->make(DatabaseInterface::class));
});
$container->bind(UserService::class, UserService::class);
$container->bind(UserController::class, UserController::class);

return $container;
<?php
// public/index.php
require __DIR__ . '/../vendor/autoload.php';

$container = require __DIR__ . '/../config/services.php';

$router = new Router();
$router->get('/users', [$container->make(UserController::class), 'index']);
$router->get('/users/{id}', [$container->make(UserController::class), 'show']);

$request = new Request();
$response = $router->dispatch($request);
$response->send();

FAQ

What is the difference between DI Container and Service Locator?

DI container creates and injects dependencies. Service locator is an anti-pattern where classes pull dependencies from a central registry. DI is preferred because dependencies are explicit.

Is autowiring slow?

Autowiring uses reflection, which has a cost. However, this happens once during construction, not on every request. Use singleton for frequently used services.

Should I always inject dependencies through constructors?

Yes, for mandatory dependencies. For optional dependencies with defaults, use setter injection or optional constructor parameters.

How do I pass scalar values through the container?

Define them as container parameters: $container->set('db.host', 'localhost'). Reference them in factory closures that create services.

What is the difference between factory and service in DI?

A factory creates objects with specific configuration. A service is the created object. Factories are often bound as services that produce other services.

What is Next

Now that you understand dependency injection, proceed to Testing with PHPUnit to learn automated testing. Then read Debugging to learn debugging techniques.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro