Основы веб-разработки

Внедрение зависимостей

Winter собирает объекты за вас. Контроллеры, сервисы, репозитории, команды и джобы создаёт DI-контейнер, разрешая их зависимости автоматически. Вы объявляете, что нужно классу, а не как это создать — никаких new в бизнес-коде.

Контейнер PSR-11Внедрение #[Autowired]Пакет winter-di

Что такое DI и зачем

DI — Dependency Injection, «внедрение зависимостей». Зависимость — это любой объект, который нужен вашему классу для работы: контроллеру нужен сервис, сервису — репозиторий, репозиторию — подключение к БД.

Проблема. Если каждый класс создаёт свои зависимости сам ($this->service = new UserService(new UserRepository(new Db(...)))), возникает жёсткая связанность: цепочку создания приходится повторять в каждом месте, подменить реализацию (на мок в тесте или другой драйвер) можно только правкой кода, а один и тот же объект пересоздаётся снова и снова.

Решение. Контейнер зависимостей берёт создание объектов на себя. Вы объявляете, что классу нужно (по типу), а контейнер сам строит зависимость, её зависимости, и внедряет готовый граф. Класс становится проще, тестируемее и не привязан к конкретным реализациям. Об этом и раздел.

Эта страница — обзор для фреймворка

Здесь — как DI используется в приложении Winter. Полный reference контейнера (разрешение, ленивые прокси, кеш рефлексии, провайдеры) — в доках Winter DI.

Как это работает

Классы-стереотипы контейнер обнаруживает автоматически при старте (тот же скан, что и для маршрутов). Когда приходит запрос, контейнер:

  1. создаёт нужный контроллер;
  2. видит его зависимости (по типам свойств и аргументов);
  3. рекурсивно создаёт их;
  4. внедряет — и вызывает обработчик.

Вам остаётся только объявить зависимость. Всё дерево собирается само.

Внедрение — #[Autowired]

Пометьте типизированное свойство атрибутом #[Autowired] — контейнер заполнит его:

php
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] Новый при каждом разрешении Когда нужен свежий экземпляр каждый раз
php
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:

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] на свойстве или аргументе конструктора. Три формы:

php
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] одну сторону цикла — её создание отложится, и цикл разомкнётся:

php
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.

Дальше