Ментальная модель
Две идеи объясняют почти всё в этом контейнере. Первая: тип-подсказка — это инструкция по проводке; контейнер читает её и строит весь граф объектов. Вторая: scope решает, будет ли каждый объект в этом графе общим или собранным заново. Удержите эти две — и остальное следует само.
Идея 1 — тип-подсказка и есть проводка
Когда вы запрашиваете класс, контейнер смотрит на его конструктор. Каждый типизированный параметр — это зависимость, которую он должен предоставить, поэтому он разрешает её так же — рекурсивно — пока не доберётся до классов без зависимостей. Затем собирает обратно.
make(UserService::class)
└─ нужен UserRepository
└─ нужен DatabaseConnection
└─ (нет зависимостей) → new DatabaseConnection
← new UserRepository(db)
← new UserService(repo)Вы не написали для этого ни строчки проводки. Объявленные типы и есть конфигурация:
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()может подстроить экземпляр под потребителя (логгер, названный по своему пользователю). См. Логгер на потребителя.
Когда к этому прибегать
- ✅ Сборка stateless-сервисов и их зависимостей без boilerplate.
- ✅ Задание классам понятных времён жизни (общий / пер-запрос / свежий).
- ❌ Протаскивание пер-запросных значений через конструкторы — передавайте их аргументами
метода или через overrides у
make(), а не как автовайренные зависимости.
Далее
- Попробовать руками: Быстрый старт
- Времена жизни подробно: Scope’ы
- Как разрешение работает на самом деле: Жизненный цикл разрешения
- Авторитетная поверхность: Справочник API