Введение в Dependency Injection в PHP
Современная разработка на PHP немыслима без принципов объектно-ориентированного проектирования. Один из ключевых паттернов, который позволяет строить гибкие, тестируемые и поддерживаемые приложения — это Dependency Injection (DI) или внедрение зависимостей.
Суть паттерна проста: вместо того чтобы создавать зависимые объекты внутри класса, мы передаем их (внедряем) извне. Это кардинально меняет архитектуру приложения, делая классы независимыми от конкретных реализаций и упрощая их тестирование.
В этой статье мы разберем, что такое Dependency Injection в PHP, какие существуют способы его реализации, и как использовать DI-контейнеры для автоматизации процесса.
Проблема: почему без DI плохо?
Рассмотрим типичный пример. Класс UserController отвечает за обработку запросов, связанных с пользователями. Он использует базу данных через класс MysqlDatabase:
<?php
class MysqlDatabase {
public function query(string $sql): array {
// ... подключение к MySQL и выполнение запроса
return [];
}
}
class UserController {
private MysqlDatabase $database;
public function __construct() {
// Жесткая привязка к конкретной реализации
$this->database = new MysqlDatabase();
}
public function listUsers(): array {
return $this->database->query('SELECT * FROM users');
}
}
$controller = new UserController();
В чем проблемы этого кода?
- Сильная связанность:
UserControllerжестко привязан кMysqlDatabase. Если мы захотим перейти на PostgreSQL или использовать файловое хранилище, придется менять код контроллера. - Сложность тестирования: Вы не сможете подменить базу данных на мок-объект в юнит-тестах. Каждый тест будет обращаться к реальной базе, что медленно и неудобно.
- Нарушение принципа единственной ответственности: Контроллер не должен знать, как создавать подключение к базе данных. Его задача — обрабатывать запросы.
Решение этой проблемы — применить принцип Инверсии управления (IoC) и использовать Dependency Injection.
Способы внедрения зависимостей в PHP
Существует три основных способа внедрить зависимость в класс:
1. Внедрение через конструктор (Constructor Injection)
Самый распространенный и рекомендуемый способ. Зависимость передается через параметр конструктора и обычно сохраняется как приватное свойство класса.
<?php
interface DatabaseInterface {
public function query(string $sql): array;
}
class MysqlDatabase implements DatabaseInterface {
public function query(string $sql): array {
// Реализация для MySQL
return [];
}
}
class PostgresDatabase implements DatabaseInterface {
public function query(string $sql): array {
// Реализация для PostgreSQL
return [];
}
}
class UserController {
private DatabaseInterface $database;
// Зависимость внедряется через конструктор
public function __construct(DatabaseInterface $database) {
$this->database = $database;
}
public function listUsers(): array {
return $this->database->query('SELECT * FROM users');
}
}
// Использование
$mysqlDb = new MysqlDatabase();
$controller = new UserController($mysqlDb);
// Легко переключаемся на другую БД
$postgresDb = new PostgresDatabase();
$controller2 = new UserController($postgresDb);
Плюсы: Зависимость явно видна, объект создается в валидном состоянии (зависимость обязательна).
2. Внедрение через сеттеры (Setter Injection)
Подходит для опциональных зависимостей или когда зависимость может быть изменена в процессе работы объекта.
<?php
class UserController {
private ?DatabaseInterface $database = null;
private ?LoggerInterface $logger = null;
public function setDatabase(DatabaseInterface $database): void {
$this->database = $database;
}
public function setLogger(LoggerInterface $logger): void {
$this->logger = $logger;
}
public function listUsers(): array {
$this->logger?->log('Fetching users...');
return $this->database->query('SELECT * FROM users');
}
}
$controller = new UserController();
$controller->setDatabase(new MysqlDatabase());
$controller->setLogger(new FileLogger()); // Опциональная зависимость
3. Внедрение через интерфейс (Interface Injection)
Реализуется реже. Класс реализует интерфейс, который обязывает его принять зависимость через специальный метод.
<?php
interface DatabaseAwareInterface {
public function setDatabase(DatabaseInterface $database): void;
}
class UserController implements DatabaseAwareInterface {
private DatabaseInterface $database;
public function setDatabase(DatabaseInterface $database): void {
$this->database = $database;
}
}
DI-контейнер: автоматизация внедрения зависимостей
Когда проект становится большим, вручную создавать цепочки зависимостей становится сложно. На помощь приходит DI-контейнер (или контейнер внедрения зависимостей).
DI-контейнер — это объект, который знает, как создавать и настраивать все классы в приложении. Он автоматически разрешает зависимости: если классу A нужен класс B, контейнер сам создаст экземпляр B и передаст его в A.
В PHP существует стандарт PSR-11 (Container Interface), который определяет базовый интерфейс для контейнеров. Популярные реализации:
- PHP-DI — мощный и гибкий контейнер с поддержкой автоподстановки.
- Symfony DependencyInjection — часть фреймворка Symfony, используется в Laravel (через Illuminate Container).
- Laravel Service Container — встроенный контейнер Laravel.
Пример работы с простым контейнером (на примере PHP-DI):
<?php
use DI\ContainerBuilder;
// 1. Создаем контейнер
$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions([
DatabaseInterface::class => function () {
return new MysqlDatabase();
},
// Можно указать, что UserController должен быть создан через контейнер
UserController::class => function (ContainerInterface $c) {
return new UserController($c->get(DatabaseInterface::class));
}
]);
$container = $containerBuilder->build();
// 2. Получаем объект с уже внедренными зависимостями
$controller = $container->get(UserController::class);
// UserController автоматически получит экзем