PHP Dependency Injection
Introduction
Dependency Injection (DI) is a design pattern that allows us to write more maintainable, testable, and flexible code by reducing tight coupling between components. It's a fundamental concept in modern PHP applications and frameworks like Laravel, Symfony, and Slim.
In simple terms, dependency injection means providing a component with its dependencies rather than letting the component create them itself. This might sound complex, but it's actually a straightforward concept that will dramatically improve your code quality once you understand it.
Understanding the Problem
Before we dive into dependency injection, let's understand the problem it solves. Consider this code:
class UserRepository {
private $database;
public function __construct() {
$this->database = new Database('localhost', 'mydb', 'user', 'password');
}
public function findById($id) {
return $this->database->query("SELECT * FROM users WHERE id = ?", [$id]);
}
}
// Usage
$userRepository = new UserRepository();
$user = $userRepository->findById(1);
This code has several problems:
- Tight coupling: The
UserRepository
class is tightly coupled to theDatabase
class - Hard to test: We cannot easily replace the database with a mock during testing
- Difficult to reuse: If we want to use a different database, we need to modify the class
Implementing Dependency Injection
Let's refactor the code to use dependency injection:
class UserRepository {
private $database;
public function __construct(Database $database) {
$this->database = $database;
}
public function findById($id) {
return $this->database->query("SELECT * FROM users WHERE id = ?", [$id]);
}
}
// Usage
$database = new Database('localhost', 'mydb', 'user', 'password');
$userRepository = new UserRepository($database);
$user = $userRepository->findById(1);
What changed?
- The
UserRepository
no longer creates its own database connection - The database dependency is "injected" through the constructor
- The class is now more flexible and testable
Types of Dependency Injection
There are three main types of dependency injection:
1. Constructor Injection
We've already seen constructor injection in our example above. Dependencies are provided through the constructor:
class UserService {
private $userRepository;
private $logger;
public function __construct(UserRepository $userRepository, Logger $logger) {
$this->userRepository = $userRepository;
$this->logger = $logger;
}
public function getUser($id) {
$this->logger->info("Fetching user with ID: $id");
return $this->userRepository->findById($id);
}
}
Advantages:
- Dependencies are clearly defined
- Objects are fully initialized after construction
- Perfect for required dependencies
2. Setter Injection
Dependencies are provided through setter methods:
class UserService {
private $userRepository;
private $logger;
public function setUserRepository(UserRepository $userRepository) {
$this->userRepository = $userRepository;
}
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
public function getUser($id) {
if ($this->logger) {
$this->logger->info("Fetching user with ID: $id");
}
return $this->userRepository->findById($id);
}
}
// Usage
$userService = new UserService();
$userService->setUserRepository(new UserRepository($database));
$userService->setLogger(new Logger());
Advantages:
- More flexible than constructor injection
- Good for optional dependencies
- Dependencies can be changed at runtime
3. Interface Injection
The class implements an interface that enforces the injection method:
interface LoggerAwareInterface {
public function setLogger(Logger $logger);
}
class UserService implements LoggerAwareInterface {
private $userRepository;
private $logger;
public function __construct(UserRepository $userRepository) {
$this->userRepository = $userRepository;
}
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
public function getUser($id) {
if ($this->logger) {
$this->logger->info("Fetching user with ID: $id");
}
return $this->userRepository->findById($id);
}
}
Advantages:
- Enforces a contract for dependency injection
- Makes the dependency requirements explicit
Dependency Injection Containers
As applications grow, manually injecting dependencies becomes cumbersome. This is where Dependency Injection Containers (DIC) come in. A DIC is responsible for:
- Creating objects
- Managing their dependencies
- Providing them when requested
Let's see a simple example of a DI container:
class Container {
private $services = [];
public function register($name, $callback) {
$this->services[$name] = $callback;
}
public function get($name) {
if (!isset($this->services[$name])) {
throw new Exception("Service '$name' not found");
}
// Get the service factory
$factory = $this->services[$name];
// Return the created service
return $factory($this);
}
}
// Usage
$container = new Container();
// Register services
$container->register('database', function($container) {
return new Database('localhost', 'mydb', 'user', 'password');
});
$container->register('user_repository', function($container) {
return new UserRepository($container->get('database'));
});
$container->register('logger', function($container) {
return new Logger();
});
$container->register('user_service', function($container) {
$service = new UserService($container->get('user_repository'));
$service->setLogger($container->get('logger'));
return $service;
});
// Get a service
$userService = $container->get('user_service');
$user = $userService->getUser(1);
This is a very simple example. In real applications, you would typically use established DI containers like PHP-DI, Symfony's DependencyInjection component, or Laravel's container.
Real-World Example: A Simple Blog Application
Let's build a simple blog application that demonstrates dependency injection in a real-world context.
First, let's define our interfaces and classes:
// Database interface
interface DatabaseInterface {
public function query($sql, $params = []);
}
// MySQL implementation
class MySQLDatabase implements DatabaseInterface {
public function __construct($host, $dbname, $user, $password) {
// Connect to MySQL database
}
public function query($sql, $params = []) {
// Execute query and return results
echo "Executing MySQL query: $sql
";
return ['Sample post data'];
}
}
// SQLite implementation
class SQLiteDatabase implements DatabaseInterface {
public function __construct($file) {
// Connect to SQLite database
}
public function query($sql, $params = []) {
// Execute query and return results
echo "Executing SQLite query: $sql
";
return ['Sample post data'];
}
}
// Post repository
class PostRepository {
private $database;
public function __construct(DatabaseInterface $database) {
$this->database = $database;
}
public function findAll() {
return $this->database->query("SELECT * FROM posts");
}
}
// Blog service
class BlogService {
private $postRepository;
public function __construct(PostRepository $postRepository) {
$this->postRepository = $postRepository;
}
public function getAllPosts() {
return $this->postRepository->findAll();
}
}
// Setup with MySQL
$mysqlDatabase = new MySQLDatabase('localhost', 'blog', 'user', 'password');
$postRepository = new PostRepository($mysqlDatabase);
$blogService = new BlogService($postRepository);
echo "Using MySQL:
";
$posts = $blogService->getAllPosts();
print_r($posts);
// Switch to SQLite without changing any business logic
$sqliteDatabase = new SQLiteDatabase('blog.sqlite');
$postRepository = new PostRepository($sqliteDatabase);
$blogService = new BlogService($postRepository);
echo "
Using SQLite:
";
$posts = $blogService->getAllPosts();
print_r($posts);
Output:
Using MySQL:
Executing MySQL query: SELECT * FROM posts
Array
(
[0] => Sample post data
)
Using SQLite:
Executing SQLite query: SELECT * FROM posts
Array
(
[0] => Sample post data
)
This example demonstrates how dependency injection allows us to:
- Switch implementations (MySQL to SQLite) without changing business logic
- Write code against interfaces rather than concrete implementations
- Create reusable components that are not tightly coupled
Benefits of Dependency Injection
Here's a visual representation of how dependency injection transforms your application architecture:
The key benefits of dependency injection include:
- Testability: You can easily substitute dependencies with mocks or stubs during testing.
- Maintainability: Components are loosely coupled, making the codebase easier to understand and modify.
- Flexibility: You can swap implementations without changing the dependent code.
- Reusability: Components are more reusable across different parts of your application.
- Separation of concerns: Each class can focus on its core responsibilities.
Dependency Injection in Popular PHP Frameworks
Most modern PHP frameworks use dependency injection extensively:
Laravel
Laravel has a powerful DI container built in:
// Register a binding
app()->bind('DatabaseInterface', function ($app) {
return new MySQLDatabase('localhost', 'mydb', 'user', 'password');
});
// Type-hinting in controllers automatically resolves dependencies
class PostController extends Controller
{
public function index(PostRepository $repository)
{
$posts = $repository->findAll();
return view('posts.index', ['posts' => $posts]);
}
}
Symfony
Symfony uses a comprehensive DI container with configuration via YAML, XML, or PHP:
// services.yaml
services:
database:
class: MySQLDatabase
arguments: ['localhost', 'mydb', 'user', 'password']
post_repository:
class: PostRepository
arguments: ['@database']
Common Pitfalls and Best Practices
Pitfalls to Avoid
- Service Locator Anti-pattern: Avoid using the container as a service locator (requesting services directly within your classes)
- Over-injection: Injecting too many dependencies can make your class bloated
- Circular Dependencies: When Class A depends on Class B, which depends on Class A
Best Practices
- Depend on abstractions: Type-hint interfaces rather than concrete implementations
- Use constructor injection for required dependencies
- Use setter injection for optional dependencies
- Keep your classes focused with a single responsibility
- Consider using autowiring when available in your framework
Exercises
- Refactor the following code to use dependency injection:
class UserController {
public function show($id) {
$db = new Database('localhost', 'mydb', 'user', 'pass');
$user = $db->query("SELECT * FROM users WHERE id = ?", [$id]);
return json_encode($user);
}
}
-
Create a simple dependency injection container from scratch that can register and resolve services.
-
Implement a blog application with dependency injection that can switch between different data sources (MySQL, SQLite, or even a file-based storage).
Summary
Dependency injection is a powerful design pattern that enables you to write more maintainable, testable, and flexible PHP applications. By injecting dependencies instead of creating them internally, you decouple your components and make your code more modular.
Key takeaways:
- DI reduces tight coupling between components
- There are three main types: constructor, setter, and interface injection
- DI containers help manage dependencies in larger applications
- Modern PHP frameworks use DI extensively
- DI makes your code more testable and maintainable
Additional Resources
- PHP-DI Documentation
- Symfony Dependency Injection Component
- Laravel Service Container
- Book: "Dependency Injection in PHP" by Fabien Potencier
- PSR-11: Container Interface
Happy coding with dependency injection!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)