Внедрение зависимостей (Dependency Injection) в PHP: полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в 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();
?>

3.2. Autowiring: Автоматическое разре

Блоги

Рекомендаций по книгам