Dependency Injection в PHP: на практике
Dependency Injection (DI) — один из ключевых принципов SOLID, который позволяет писать гибкий, тестируемый и поддерживаемый код. В этой статье мы разберем, как реализовать внедрение зависимостей в PHP на реальных примерах, без абстрактных теорий. Вы узнаете, как избавиться от жестких связей между классами и сделать ваш код готовым к изменениям.
Что такое Dependency Injection и зачем он нужен?
Представьте, что у вас есть класс UserController, который работает с базой данных. Без DI вы, скорее всего, создаете объект подключения прямо внутри контроллера:
class UserController {
private $db;
public function __construct() {
$this->db = new MySQLConnection('localhost', 'user', 'pass');
}
}
Такой подход создает жесткую связь (tight coupling). Если вы захотите переключиться на PostgreSQL или использовать мок-объект для тестов, придется менять код контроллера. Dependency Injection решает эту проблему: зависимости передаются извне, а не создаются внутри класса.
Основные преимущества DI:
- Снижение связанности кода
- Упрощение тестирования (легко подменять зависимости)
- Повторное использование классов
- Чистая архитектура и соблюдение принципа инверсии зависимостей (DIP)
Способы внедрения зависимостей в PHP
Существует три основных способа инъекции зависимостей. Рассмотрим каждый с примерами.
1. Внедрение через конструктор
Самый распространенный и рекомендуемый способ. Зависимости передаются через параметры конструктора и сохраняются в свойствах класса.
<?php
interface DatabaseInterface {
public function query(string $sql): array;
}
class MySQLDatabase implements DatabaseInterface {
public function query(string $sql): array {
// Реальная логика работы с MySQL
return ['id' => 1, 'name' => 'John'];
}
}
class UserRepository {
private DatabaseInterface $db;
public function __construct(DatabaseInterface $db) {
$this->db = $db;
}
public function getUser(int $id): array {
return $this->db->query("SELECT * FROM users WHERE id = $id");
}
}
// Использование:
$db = new MySQLDatabase();
$repo = new UserRepository($db);
$user = $repo->getUser(1);
?>
Обратите внимание: мы используем интерфейс DatabaseInterface. Это позволяет легко подменить реализацию — например, на тестовую заглушку.
2. Внедрение через сеттеры
Используется, когда зависимость не обязательна на этапе создания объекта или может меняться в процессе работы.
<?php
class MailService {
private ?MailerInterface $mailer = null;
public function setMailer(MailerInterface $mailer): void {
$this->mailer = $mailer;
}
public function send(string $to, string $subject, string $body): void {
if ($this->mailer === null) {
throw new \RuntimeException('Mailer not configured');
}
$this->mailer->send($to, $subject, $body);
}
}
// Использование:
$mailService = new MailService();
$mailService->setMailer(new SmtpMailer());
$mailService->send('user@example.com', 'Hello', 'Body');
?>
Минус подхода — вы не гарантируете, что зависимость будет установлена до вызова метода send(). Поэтому для обязательных зависимостей предпочтительнее конструктор.
3. Внедрение через метод
Зависимость передается непосредственно в метод, где она нужна. Полезно для редких или контекстных зависимостей.
<?php
class ReportGenerator {
public function generate(DataFetcherInterface $fetcher, string $reportType): array {
$data = $fetcher->fetch($reportType);
// Обработка данных...
return ['report' => $data];
}
}
// Использование:
$generator = new ReportGenerator();
$report = $generator->generate(new APIDataFetcher(), 'monthly');
?>
Контейнер зависимостей (Dependency Injection Container)
Когда проект разрастается, ручное создание объектов с зависимостями становится утомительным. На помощь приходит DI-контейнер — объект, который управляет созданием и хранением зависимостей.
Рассмотрим простую реализацию контейнера:
<?php
class SimpleContainer {
private array $bindings = [];
private array $instances = [];
public function set(string $id, callable $resolver): void {
$this->bindings[$id] = $resolver;
}
public function get(string $id): mixed {
if (!isset($this->instances[$id])) {
$this->instances[$id] = $this->bindings[$id]($this);
}
return $this->instances[$id];
}
}
// Настройка контейнера
$container = new SimpleContainer();
$container->set(DatabaseInterface::class, function($c) {
return new MySQLDatabase();
});
$container->set(UserRepository::class, function($c) {
return new UserRepository($c->get(DatabaseInterface::class));
});
// Использование — получаем готовый объект со всеми зависимостями
$repo = $container->get(UserRepository::class);
$user = $repo->getUser(1);
?>
В реальных проектах чаще используют готовые библиотеки, такие как PHP-DI, Symfony DependencyInjection или Laravel Service Container. Они умеют автоматически определять зависимости через рефлексию (autowiring).
Практический пример: приложение с DI
Соберем все вместе — создадим мини-приложение, которое отправляет уведомления пользователям через разные каналы.
<?php
// 1. Интерфейсы
interface NotificationChannel {
public function send(string $to, string $message): void;
}
interface UserRepositoryInterface {
public function findByEmail(string $email): ?array;
}
// 2. Реализации
class EmailChannel implements NotificationChannel {
public function send(string $to, string $message): void {
echo "Email to {$to}: {$message}\n";
}
}
class SMSChannel implements NotificationChannel {
public function send(string $to, string $message): void {
echo "SMS to {$to}: {$message}\n";
}
}
class UserRepository implements UserRepositoryInterface {
private array $users = [
'john@example.com' => ['name' => 'John', 'phone' => '+1234567890'],
'jane@example.com' =>