Пакет · di

Ментальная модель

Две идеи объясняют почти всё в этом контейнере. Первая: тип-подсказка — это инструкция по проводке; контейнер читает её и строит весь граф объектов. Вторая: scope решает, будет ли каждый объект в этом графе общим или собранным заново. Удержите эти две — и остальное следует само.

Идея 1 — тип-подсказка и есть проводка

Когда вы запрашиваете класс, контейнер смотрит на его конструктор. Каждый типизированный параметр — это зависимость, которую он должен предоставить, поэтому он разрешает её так же — рекурсивно — пока не доберётся до классов без зависимостей. Затем собирает обратно.

text
make(UserService::class)
└─ нужен UserRepository
     └─ нужен DatabaseConnection
          └─ (нет зависимостей) → new DatabaseConnection
     ← new UserRepository(db)
← new UserService(repo)

Вы не написали для этого ни строчки проводки. Объявленные типы и есть конфигурация:

php
class UserService
{
  public function __construct(private UserRepository $repo) {}
}

Практическое следствие: вы перестаёте вызывать new для сервисов. Вы описываете, что нужно классу, а контейнер это собирает. Интерфейсы — единственный случай, требующий помощи: контейнер не может угадать, какую реализацию вы имеете в виду, поэтому вы говорите ему это один раз привязкой (см. Сервис-провайдеры).

Идея 2 — scope решает совместное использование

Построение графа — лишь половина истории. Другая половина — идентичность: когда двум классам нужен DatabaseConnection, получат ли они один и тот же или каждый свой? Это и есть scope.

Scope Идентичность Строится
singleton один общий экземпляр на процесс один раз, затем кэшируется
request один на HTTP-запрос / корутину один раз на запрос
transient новый экземпляр каждый раз при каждом разрешении

Класс выбирает scope атрибутом (#[Singleton], #[Request], #[Transient]) или вы задаёте его привязкой. Без атрибута и без привязки scope — transient — безопасный вариант по умолчанию, потому что свежий объект никогда не утечёт состоянием.

Почему по умолчанию transient

Общий объект, хранящий пер-запросное состояние (текущий пользователь, id запроса), утечёт это состояние между запросами — реальная опасность в долгоживущих Swoole-воркерах. Значение по умолчанию transient означает, что ничто не разделяется, пока вы намеренно этого не скажете. Полное руководство — в Scope’ах.

Складываем вместе

Разрешение обходит граф (Идея 1); в каждом узле scope решает, переиспользовать кэшированный экземпляр или собрать новый (Идея 2). Так один #[Singleton] DatabaseConnection, общий для многих репозиториев, строится один раз, а #[Transient] QueryBuilder свеж для каждого владельца — всё в рамках одного разрешения.

Что из этого следует

  • Атрибуты на свойствах — иногда внедрить через конструктор нельзя (им владеет базовый класс). #[Autowired] / #[Inject] на свойстве внедряют после конструирования, всё так же по типу. См. Внедрение в свойства.
  • Настоящий цикл — это ошибка — если A нужен B, а B нужен A, рекурсия не завершится. Контейнер обнаруживает это и бросает исключение. Когда цикл легитимен, #[Lazy] внедряет proxy, чтобы одна сторона разрешилась позже — см. Разрыв циклических зависимостей.
  • Потребитель может формировать зависимость — поскольку контейнер знает, в какой класс он внедряет, фабрика contextual() может подстроить экземпляр под потребителя (логгер, названный по своему пользователю). См. Логгер на потребителя.

Когда к этому прибегать

Отлично для проводки сервисов, контроллеров, команд по типуНе для передачи runtime-данных (id, payload запроса) в конструкторы
  • ✅ Сборка stateless-сервисов и их зависимостей без boilerplate.
  • ✅ Задание классам понятных времён жизни (общий / пер-запрос / свежий).
  • ❌ Протаскивание пер-запросных значений через конструкторы — передавайте их аргументами метода или через overrides у make(), а не как автовайренные зависимости.

Далее