Внедрение зависимостей
Winter собирает объекты за вас. Контроллеры, сервисы, репозитории, команды и
джобы создаёт DI-контейнер, разрешая их зависимости автоматически. Вы объявляете,
что нужно классу, а не как это создать — никаких new в бизнес-коде.
Что такое DI и зачем
DI — Dependency Injection, «внедрение зависимостей». Зависимость — это любой объект, который нужен вашему классу для работы: контроллеру нужен сервис, сервису — репозиторий, репозиторию — подключение к БД.
Проблема. Если каждый класс создаёт свои зависимости сам ($this->service = new UserService(new UserRepository(new Db(...)))), возникает жёсткая связанность:
цепочку создания приходится повторять в каждом месте, подменить реализацию (на
мок в тесте или другой драйвер) можно только правкой кода, а один и тот же объект
пересоздаётся снова и снова.
Решение. Контейнер зависимостей берёт создание объектов на себя. Вы объявляете, что классу нужно (по типу), а контейнер сам строит зависимость, её зависимости, и внедряет готовый граф. Класс становится проще, тестируемее и не привязан к конкретным реализациям. Об этом и раздел.
Эта страница — обзор для фреймворка
Здесь — как DI используется в приложении Winter. Полный reference контейнера (разрешение, ленивые прокси, кеш рефлексии, провайдеры) — в доках Winter DI.
Как это работает
Классы-стереотипы контейнер обнаруживает автоматически при старте (тот же скан, что и для маршрутов). Когда приходит запрос, контейнер:
- создаёт нужный контроллер;
- видит его зависимости (по типам свойств и аргументов);
- рекурсивно создаёт их;
- внедряет — и вызывает обработчик.
Вам остаётся только объявить зависимость. Всё дерево собирается само.
Внедрение — #[Autowired]
Пометьте типизированное свойство атрибутом #[Autowired] — контейнер заполнит его:
use Flytachi\Winter\DI\Attribute\Autowired;
class UserService extends Service
{
#[Autowired] private UserRepository $repo;
#[Autowired] private LoggerInterface $logger;
public function find(int $id): User
{
$this->logger->info('lookup', ['id' => $id]);
return $this->repo->findByIdOrThrow($id);
}
}Работает в любом классе, который строит контейнер: контроллерах, сервисах, репозиториях, командах, джобах, middleware.
Жизненные циклы
Каждый класс живёт в одном из трёх режимов (scope). По умолчанию сервисы и репозитории ведут себя как singleton’ы; явно режим задаётся атрибутом на классе:
| Атрибут | Жизнь экземпляра | Когда применять |
|---|---|---|
#[Singleton] |
Один на процесс | Без состояния запроса: сервисы, репозитории, клиенты |
#[Request] |
Один на HTTP-запрос | Состояние запроса: контекст аутентификации, текущий пользователь |
#[Transient] |
Новый при каждом разрешении | Когда нужен свежий экземпляр каждый раз |
use Flytachi\Winter\DI\Attribute\Request;
#[Request] // один на запрос
class AuthContext
{
private ?User $user = null;
public function user(): ?User { return $this->user; }
public function setUser(User $user): void { $this->user = $user; }
}Request-scope для контекста запроса
#[Request] — идеальный носитель данных текущего запроса. Middleware
аутентификации кладёт пользователя в AuthContext, а любой контроллер или сервис
получает тот же экземпляр через #[Autowired] — в пределах одного запроса.
Ручные привязки — Boot::providers()
Автовайринг покрывает конкретные классы. Когда нужно связать интерфейс с
реализацией, задать фабрику или скалярное значение — используйте хук
providers() в bootstrap.php:
protected static function providers(Container $c): void
{
// Интерфейс → конкретная реализация (singleton)
$c->singleton(CacheInterface::class, RedisCache::class);
// Фабрика с собственной логикой построения
$c->bind(MailerInterface::class, fn(Container $c) =>
new SmtpMailer(env('MAIL_HOST'), $c->make(LoggerInterface::class))
);
// Скалярное значение по ключу
$c->set('config.timeout', (int) env('APP_TIMEOUT', 30));
// Провайдер — группа связанных привязок
$c->register(AppServiceProvider::class);
}Основные методы контейнера:
| Метод | Что делает |
|---|---|
singleton($abstract, $concrete) |
Привязка с единственным экземпляром на процесс |
request($abstract, $concrete) |
Привязка с экземпляром на запрос |
transient($abstract, $concrete) |
Новый экземпляр при каждом разрешении |
bind($abstract, $concrete) |
Привязка класса или фабрики-замыкания |
set($id, $value) |
Готовое значение по ключу |
register($providerClass) |
Подключить сервис-провайдер |
make($abstract, $overrides) |
Создать экземпляр вручную |
Переопределение — #[Inject]
#[Autowired] разрешает зависимость по типу. Когда нужно указать конкретную
реализацию или именованное значение — используйте #[Inject] на свойстве или
аргументе конструктора. Три формы:
use Flytachi\Winter\DI\Attribute\Inject;
class ReportService extends Service
{
public function __construct(
// 1. По типу — явный маркер, как автовайринг
#[Inject] private CacheInterface $cache,
// 2. Конкретная реализация — в обход глобальной привязки
#[Inject(FileCache::class)] private CacheInterface $fallback,
// 3. Именованное значение по ключу (см. $c->set() выше)
#[Inject('config.timeout')] private int $timeout,
) {}
}На свойстве #[Inject(RedisCache::class)] работает так же — внедрение происходит
после конструктора.
Ленивое внедрение — #[Lazy]
#[Lazy] откладывает создание зависимости: вместо готового объекта контейнер
внедряет ленивый прокси (нативный прокси PHP 8.4), а реальный экземпляр
разрешается из контейнера при первом обращении к нему.
Главное применение — разорвать циклическую зависимость. Если сервис A нужен
сервису B, а B нужен A, прямое разрешение зациклится. Достаточно пометить
#[Lazy] одну сторону цикла — её создание отложится, и цикл разомкнётся:
use Flytachi\Winter\DI\Attribute\Lazy;
class SmsSendService extends Service
{
public function __construct(
// прокси; настоящий PeerService создастся при первом использовании
#[Lazy] private PeerService $peer,
) {}
}Прокси нужен конкретный класс
Ленивый прокси подменяет собой конкретный класс. Указывайте тип напрямую или
через #[Inject(Concrete::class), Lazy] — интерфейс без конкретной реализации
бросит исключение (проксировать нечего).
Полный разбор ленивых прокси и разрыва циклов — в доках Winter DI.
Под капотом
Как контейнер находит и создаёт зависимости, и почему это быстро. Для повседневной работы знать не обязательно.
- Автообнаружение. При старте сканер проходит PSR-4-корни и регистрирует классы
со scope-атрибутами (
#[Singleton]/#[Request]/#[Transient]) — тот же проход, что собирает маршруты. - Кеш DI. В продакшене (
DEBUG=false) граф зависимостей сериализуется вdi.php, чтобы не сканировать на каждый старт. Соберите его заранее:php call di build. ПриDEBUG=trueконтейнер пересканирует на каждый запрос — новые сервисы видны сразу. - Кеш рефлексии. Разобранные конструкторы и свойства хранятся в кеше рефлексии — повторное создание объекта не платит за рефлексию.
Посмотреть, что зарегистрировано в контейнере: php call di show.
Дальше
- Winter DI — полный reference — разрешение, scope’ы, прокси, провайдеры
- Контроллеры — где чаще всего наполняется
#[Autowired] - Middleware — как заполняется request-scope перед контроллером