PHP Dependency Injection — Constructor Injection, Autowiring, and Container Patterns
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
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.
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.
What is autowiring? Autowiring uses reflection to inspect constructor parameters and automatically resolve them from the container. It reduces manual configuration.
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.
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 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