Dependency Injection в PHP: Полное руководство с примерами

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

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

Начать курс

Введение в 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 автоматически получит экзем                

Блоги

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