Введение в Dependency Injection (DI) в PHP
Современная разработка на PHP невозможна без понимания принципов слабой связности компонентов. Dependency Injection (DI), или внедрение зависимостей, — это один из ключевых паттернов проектирования, который позволяет создавать гибкие, тестируемые и легко поддерживаемые приложения. Вместо того чтобы класс сам создавал свои зависимости (например, объект базы данных или логгера), он получает их извне. Это кардинально меняет архитектуру кода.
В этой статье мы подробно разберём, что такое Dependency Injection в PHP, какие существуют способы внедрения, как работает DI-контейнер и почему этот паттерн стал стандартом для современных фреймворков, таких как Laravel, Symfony и Yii2.
1. Проблема: Жёсткая связность (Tight Coupling)
Прежде чем говорить о решении, давайте посмотрим на проблему. Классический пример — класс UserController, который напрямую создаёт объект Database внутри себя:
<?php
class Database {
public function query(string $sql): array {
// Подключение к БД и выполнение запроса
return [];
}
}
class UserController {
private Database $db;
public function __construct() {
// Жёсткая зависимость — создаём объект внутри конструктора
$this->db = new Database();
}
public function getUsers(): array {
return $this->db->query('SELECT * FROM users');
}
}
?>
Недостатки такого подхода:
- Сложность тестирования: Вы не сможете подменить
Databaseна mock-объект в юнит-тестах. - Нарушение принципа единственной ответственности (SRP): Класс отвечает не только за бизнес-логику, но и за создание зависимостей.
- Сложность модификации: Если вы захотите заменить MySQL на PostgreSQL, придётся менять код во всех классах, где создаётся
Database.
2. Решение: Внедрение зависимостей (Dependency Injection)
Dependency Injection предлагает передавать зависимости извне. Существует три основных способа внедрения: через конструктор, через сеттеры и через интерфейс (метод). Рассмотрим каждый.
2.1. Внедрение через конструктор (Constructor Injection)
Самый распространённый и рекомендуемый способ. Зависимости передаются один раз при создании объекта и не могут быть изменены в дальнейшем.
<?php
class UserController {
private Database $db;
// Зависимость передаётся извне
public function __construct(Database $db) {
$this->db = $db;
}
public function getUsers(): array {
return $this->db->query('SELECT * FROM users');
}
}
// Использование:
$db = new Database();
$controller = new UserController($db);
?>
Плюсы: Гарантирует, что зависимость всегда доступна; идеально для обязательных зависимостей. Минусы: Если у класса много зависимостей, конструктор становится громоздким.
2.2. Внедрение через сеттеры (Setter Injection)
Подходит для опциональных зависимостей, которые могут быть установлены после создания объекта.
<?php
class Logger {
public function log(string $message): void {
echo $message;
}
}
class UserController {
private ?Logger $logger = null;
public function setLogger(Logger $logger): void {
$this->logger = $logger;
}
public function getUsers(): array {
if ($this->logger) {
$this->logger->log('Fetching users...');
}
// Логика получения пользователей
return [];
}
}
// Использование:
$controller = new UserController();
$controller->setLogger(new Logger());
?>
Плюсы: Гибкость, возможность изменять зависимость в процессе работы. Минусы: Объект может находиться в невалидном состоянии, если забыть вызвать сеттер.
2.3. Внедрение через интерфейс (Interface Injection)
Реализуется через метод, принимающий зависимость. Часто используется в комбинации с контейнерами.
<?php
interface LoggerAwareInterface {
public function setLogger(Logger $logger): void;
}
class UserController implements LoggerAwareInterface {
private Logger $logger;
public function setLogger(Logger $logger): void {
$this->logger = $logger;
}
public function getUsers(): array {
$this->logger->log('Fetching users...');
return [];
}
}
?>
3. DI-контейнер: Автоматизация внедрения зависимостей
Когда в приложении сотни классов, ручное создание и передача зависимостей становятся утомительными. На помощь приходит DI-контейнер (Service Container). Это инструмент, который автоматически анализирует зависимости классов (через Reflection или явные определения) и создаёт объекты с нужными зависимостями.
В PHP существует стандарт PSR-11: Container Interface, который определяет единый интерфейс для контейнеров.
3.1. Пример простого DI-контейнера
Реализуем простой контейнер, соответствующий PSR-11:
<?php
use Psr\Container\ContainerInterface;
class SimpleContainer implements ContainerInterface {
private array $bindings = [];
private array $instances = [];
public function set(string $id, callable $factory): void {
$this->bindings[$id] = $factory;
}
public function get(string $id): mixed {
if (isset($this->instances[$id])) {
return $this->instances[$id];
}
if (!$this->has($id)) {
throw new \RuntimeException("Service '$id' not found");
}
$this->instances[$id] = call_user_func($this->bindings[$id], $this);
return $this->instances[$id];
}
public function has(string $id): bool {
return isset($this->bindings[$id]);
}
}
// Регистрируем зависимости
$container = new SimpleContainer();
$container->set(Database::class, function ($c) {
return new Database();
});
$container->set(UserController::class, function ($c) {
return new UserController($c->get(Database::class));
});
// Использование
$controller = $container->get(UserController::class);
$users = $controller->getUsers();
?>