Контейнер¶
PSR-11 2.0 контейнер с autowiring по конструктору, атрибутами, lazy-объектами PHP 8.4,
сбросом состояния между запросами и компиляцией в PHP-класс. Namespace Keel\Container.
Две реализации, одно поведение¶
Container |
скомпилированный класс | |
|---|---|---|
| Когда | dev, тесты | prod |
| Как создаётся | new Container($definitions) |
new Compiler($definitions)->write($path), затем new KeelCompiledContainer($instances) |
| Разрешение зависимостей | рефлексия на каждый первый get() |
сгенерированный код, рефлексия только для классов, не известных при компиляции |
Оба реализуют Keel\Container\ContainerInterface (PSR-11 плюс tagged() и reset()) и проходят
один и тот же набор тестов (tests/Container/ContainerBehaviourTestCase.php).
Регистрация¶
use Keel\Container\{Container, Definitions, Ref};
$d = new Definitions();
$d->parameter('db.dsn', 'pgsql://db'); // скаляр, массив или enum
$d->instance(HttpConfig::class, $config); // готовый объект
$d->set(SystemClock::class); // читает атрибуты класса
$d->set(Connection::class)
->factory(ConnectionFactory::class . '::create') // статический метод
->lazy();
$d->set('mailer.special', Mailer::class)
->argument('logger', new Ref('logger.special')); // явный аргумент
$d->alias(Clock::class, SystemClock::class);
$d->load(new HttpProvider(), new DbProvider()); // ServiceProvider::register(Definitions)
$container = new Container($d);
Класс, который нигде не зарегистрирован, всё равно создаётся по запросу, если он
инстанцируемый и все параметры конструктора разрешимы. Autowiring выключается вторым
аргументом конструктора любого из контейнеров (autowire: false), и значит это одно и то же:
разрешаются только зарегистрированные определения, обязательная зависимость от
незарегистрированного класса падает с цепочкой разрешения, необязательная получает умолчание.
Скомпилированный класс помнит, какие классы он автовайрил при компиляции (AUTOWIRED), и при
выключенном флаге их не использует.
Порядок поиска в get()/has(), одинаковый в обоих контейнерах: готовые объекты, затем
определения (зарегистрированные или автовайринг), затем параметры.
Id — либо алиас, либо определение, но не оба сразу: alias('x', ...) после set('x'),
set('x') после alias('x', ...) и #[Alias('x')]/->alias('x'), совпадающий с
определением x, дают ContainerException вместо молчаливого затенения одного другим.
Регистрируйте реализацию под её собственным id и ведите алиас интерфейса на неё.
Единственное исключение — set(Iface::class, Impl::class), когда Impl несёт
#[Alias(Iface::class)]: алиас на собственный id игнорируется. Петли алиасов отклоняются
при создании контейнера.
Фабричные методы должны быть публичными: private или protected Class::method отвергают оба
контейнера с ошибкой Factory Class::method of "id" must be public (рефлексия смогла бы его
вызвать, сгенерированный код — нет).
Разрешение аргументов конструктора и фабрики¶
Для каждого параметра в этом порядке:
- явный
->argument('name', value);Refразрешается из контейнера, остальное подставляется как есть; #[Inject('id')]— сервис или параметр по id;#[TaggedIterator('tag')]— список всех сервисов с тегом;- типизированный классом/интерфейсом параметр — сервис по имени типа; если у параметра есть
значение по умолчанию или он nullable (
?Foo $foo), сервис подставляется только когда контейнер его знает (has()), иначе передаётся умолчание (nullдля nullable); - значение по умолчанию;
nullдля nullable-типа;- иначе
ContainerExceptionс именем параметра и класса.
Атрибуты класса¶
| Атрибут | Эффект |
|---|---|
#[Shared] |
один экземпляр на контейнер (по умолчанию) |
#[Transient] |
новый экземпляр на каждый get(); не участвует в reset() |
#[Lazy] |
newLazyGhost: конструктор выполняется при первом обращении к свойству, не при get(); для фабрик — newLazyProxy |
#[Alias(Iface::class)] |
id Iface ведёт на этот класс (повторяемый) |
#[Tag('name')] |
участвует в tagged('name') и #[TaggedIterator('name')] (повторяемый) |
Атрибуты читаются при Definitions::set() и при autowiring незарегистрированного класса.
Fluent-вызовы после set() имеют приоритет над атрибутами. Явный Definitions::alias()
имеет приоритет над #[Alias].
Важно про lazy: вызов метода не инициализирует ghost, инициализацию запускает чтение или
запись свойства. Lazy-сервис разрывает циклические зависимости: A(B), #[Lazy] B(A) работает.
Сброс между запросами¶
Сервис, накапливающий состояние запроса, реализует Keel\Container\Resettable. В worker-режиме
ядро вызывает $container->reset() после каждого запроса: обходятся только созданные
shared-сервисы, неинициализированные lazy-объекты пропускаются. Исключение в одном reset()
не останавливает остальные: обходятся все сервисы, затем бросается один ContainerException
со списком сбоев (первый — в getPrevious()).
Компиляция¶
new Compiler($d)->write('var/cache/container.php', 'KeelCompiledContainer', 'App\Generated');
require 'var/cache/container.php';
$container = new \App\Generated\KeelCompiledContainer([HttpConfig::class => $config]);
Компилируются все определения и все классы, достижимые из них через типы конструкторов.
Объекты из Definitions::instance() в код не попадают: их id перечисляются в
INSTANCE_IDS, и конструктор скомпилированного класса требует их на входе. Имя класса и
namespace должны быть корректными идентификаторами PHP; id сервисов — произвольные строки,
они экранируются.
Сгенерированный класс несёт CONTENT_HASH — Compiler::contentHash() собственного файла.
PHP не умеет перезагружать класс, поэтому App сравнивает их, когда класс уже загружен,
и отказывается использовать кэш, переписанный в том же процессе.
Не компилируются и дают ContainerException на этапе компиляции:
- фабрики-замыкания и фабрики
[$object, 'method']— замените на'Class::method'или[Class::class, 'method'](экземпляр метода берётся из контейнера); - аргументы и параметры с объектами — регистрируйте их через
instance(); - lazy-сервисы с непубличным конструктором.
Запись файла атомарная (временный файл + rename).
Ошибки¶
NotFoundException(PSRNotFoundExceptionInterface) — запрошенный id неизвестен и не автовайрится. «Не найденным» бывает только id, переданный вget(): зависимость, отсутствующая глубже в цепочке, — этоContainerExceptionс цепочкой разрешения в сообщении, ведьhas()для запрошенного id возвращает true;CircularDependencyException— цикл без lazy-звена, в сообщении вся цепочка;ContainerException— всё остальное; сообщение содержит цепочку разрешения, когда она есть.
Отсутствующая зависимость несёт и диагноз: $dependency, $reason (не зарегистрирована,
autowiring выключен, интерфейс или абстрактный класс, непубличный конструктор), $autowire и
$chain, а report() раскладывает их для консоли — именно это печатают keel:compile и
keel:doctor:
Cannot resolve service App\Service\OrderService
Dependency: App\Repository\OrderRepository
Reason: autowiring is disabled and the class is not registered
Autowiring: disabled
Resolution path:
App\Controller\OrderController -> App\Service\OrderService -> App\Repository\OrderRepository
new Diagnostics($definitions, $autowire)->check() выполняет то же разрешение для каждого
зарегистрированного сервиса, алиаса и тега, ничего не создавая, и возвращает по такому исключению
на проблему; keel:doctor и тест на ваши Definitions упадут раньше первого запроса.