Что такое Dependency Injection в PHP и зачем это нужно?
Представьте, что вы строите дом. Вместо того чтобы самому делать каждый кирпич, вы просто заказываете их и используете готовые. Dependency Injection (DI) в программировании работает по тому же принципу — вы не создаёте объекты внутри класса, а получаете их извне. Это делает ваш код гибким, тестируемым и понятным.
В PHP, как и в любом объектно-ориентированном языке, классы часто зависят от других классов. Проблема возникает, когда зависимости создаются прямо внутри класса — это называется жёсткой связью (tight coupling). Dependency Injection решает эту проблему, передавая зависимости извне.
Проблема без Dependency Injection
Рассмотрим типичный пример, когда класс UserService сам создаёт объект базы данных:
<?php
class Database {
public function query($sql) {
// Логика запроса
return "Результат: " . $sql;
}
}
class UserService {
private $db;
public function __construct() {
// Жёсткая связь — класс сам создаёт зависимость
$this->db = new Database();
}
public function getUser($id) {
return $this->db->query("SELECT * FROM users WHERE id = $id");
}
}
$service = new UserService();
echo $service->getUser(1);
?>
Проблемы такого подхода:
- Нельзя заменить
Databaseна другую реализацию (например, для тестов) - Сложно тестировать — нельзя подставить заглушку (mock)
- Код становится монолитным и трудным для расширения
Решение: Внедрение зависимости через конструктор
Теперь перепишем тот же класс, используя Dependency Injection:
<?php
class UserService {
private $db;
// Зависимость передаётся извне
public function __construct(Database $db) {
$this->db = $db;
}
public function getUser($id) {
return $this->db->query("SELECT * FROM users WHERE id = $id");
}
}
// Создаём зависимость отдельно
$database = new Database();
$service = new UserService($database);
echo $service->getUser(1);
?>
Теперь UserService не знает, как создаётся Database. Он просто использует то, что ему передали. Это и есть внедрение зависимости.
Виды Dependency Injection в PHP
Существует три основных способа внедрения зависимостей. Рассмотрим каждый с примерами.
1. Внедрение через конструктор (Constructor Injection)
Самый распространённый и рекомендуемый способ. Зависимости передаются при создании объекта через конструктор.
<?php
class Logger {
public function log($message) {
echo "[LOG]: " . $message;
}
}
class OrderProcessor {
private $logger;
// Зависимость в конструкторе
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function process($order) {
// Обработка заказа
$this->logger->log("Заказ обработан: " . $order);
}
}
$logger = new Logger();
$processor = new OrderProcessor($logger);
$processor->process("Заказ #123");
?>
Плюсы: зависимость всегда доступна, объект всегда в валидном состоянии. Минусы: если зависимостей много, конструктор может стать громоздким.
2. Внедрение через сеттер (Setter Injection)
Используется, когда зависимость может быть необязательной или её нужно заменить в процессе работы объекта.
<?php
class MailService {
private $transport;
// Сеттер для зависимости
public function setTransport($transport) {
$this->transport = $transport;
}
public function send($to, $message) {
if ($this->transport) {
echo "Отправка " . $message . " на " . $to . " через " . $this->transport;
} else {
echo "Транспорт не настроен!";
}
}
}
$mail = new MailService();
$mail->setTransport("SMTP");
$mail->send("user@example.com", "Привет!");
?>
Плюсы: гибкость, можно менять зависимость в рантайме. Минусы: можно забыть вызвать сеттер, и объект будет неполным.
3. Внедрение через интерфейс (Interface Injection)
Используется реже, но полезен для сложных систем. Класс реализует интерфейс, который обязывает принять зависимость.
<?php
interface LoggerAwareInterface {
public function setLogger(Logger $logger);
}
class UserManager implements LoggerAwareInterface {
private $logger;
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
public function createUser($name) {
// Создание пользователя
if ($this->logger) {
$this->logger->log("Создан пользователь: " . $name);
}
}
}
$logger = new Logger();
$manager = new UserManager();
$manager->setLogger($logger);
$manager->createUser("Иван");
?>
Практический пример: Рефакторинг с Dependency Injection
Давайте посмотрим, как DI улучшает реальный код. Представим, что у нас есть класс для отправки уведомлений.
Плохой код (без DI)
<?php
class NotificationService {
private $emailSender;
private $smsSender;
public function __construct() {
// Жёсткая связь — класс сам создаёт все зависимости
$this->emailSender = new EmailSender();
$this->smsSender = new SmsSender();
}
public function notify($user, $message) {
$this->emailSender->send($user->email, $message);
$this->smsSender->send($user->phone, $message);
}
}
?>
Хороший код (с DI)
<?php
interface SenderInterface {
public function send($recipient, $message);
}
class EmailSender implements SenderInterface {
public function send($recipient, $message) {
echo "Email отправлен на " . $recipient . ": " . $message;
}
}
class SmsSender implements SenderInterface {
public function send($recipient, $message) {
echo "SMS отправлен на " . $recipient . ": "