Пакет · di

Авторегистрация аннотированных классов

Пометка класса #[Singleton] объявляет его scope, но кто-то должен прочитать этот атрибут и зарегистрировать привязку. Scanner проходит по дереву проекта один раз и передаёт каждый класс в DICollector, который регистрирует те, у кого есть scope-атрибут — так что вам не нужно перечислять их вручную.

Базовое сканирование

Наведите Scanner на корень исходников, подключите DICollector, привязанный к контейнеру, и запустите.

bootstrap.php
<?php

use Flytachi\Winter\DI\Container;
use Flytachi\Winter\DI\Scanner;
use Flytachi\Winter\DI\Collector\DICollector;

$container = Container::init();

Scanner::run(__DIR__ . '/src')
  ->collect(new DICollector($container))
  ->execute();

DICollector сопоставляет каждый scope-атрибут соответствующей привязке:

Атрибут Регистрация
#[Singleton] $container->singleton($class)
#[Request] $container->request($class)
#[Transient] $container->transient($class)

Класс без scope-атрибута коллектор пропускает — он остаётся автовайримым по требованию как transient.

Что Scanner пропускает

Абстрактные классы, интерфейсы и трейты отфильтровываются до запуска коллекторов, поэтому collect() всегда видит только инстанцируемые классы. vendor/ исключается всегда.

Добавьте продакшн-кэш

Сканировать файловую систему при каждом старте расточительно в продакшене. Передайте путь cache: первый старт обходит дерево и записывает список найденных имён классов; последующие старты загружают этот список и полностью пропускают обход.

php
Scanner::run(__DIR__ . '/src', cache: __DIR__ . '/var/cache/di.php')
  ->collect(new DICollector($container))
  ->execute();

Файл кэша — обычный PHP-массив полностью квалифицированных имён классов:

var/cache/di.php
<?php

return [
  'App\\Service\\UserService',
  'App\\Repository\\UserRepository',
  // ...
];

Инвалидация кэша

Кэш хранит список классов, а не привязки. Добавление, переименование или перемещение класса делает список устаревшим — удалите файл кэша, чтобы принудительно пересканировать при следующем старте. Встройте это в шаг деплоя.

Исключение каталогов

vendor/ исключается всегда. Добавьте другие пути через exclude() (абсолютные пути):

php
Scanner::run(__DIR__ . '/src')
  ->exclude([
      __DIR__ . '/src/legacy',
      __DIR__ . '/src/generated',
  ])
  ->collect(new DICollector($container))
  ->execute();

Запуск нескольких коллекторов за один проход

DICollector — один коллектор; у фреймворка их обычно больше (маршруты, обработчики исключений, консольные команды). Зарегистрируйте их все на одном Scanner, и они разделят один обход файловой системы — ни один класс не читается дважды.

php
Scanner::run(__DIR__ . '/src', cache: $cachePath)
  ->collect(new DICollector($container))     // scope-атрибуты
  ->collect(new MappingCollector($router))   // атрибуты маршрутов
  ->collect(new ExceptionCollector())        // обработчики исключений
  ->execute();

Коллекторы вызываются в порядке регистрации для каждого класса.

Напишите свой коллектор

Любой класс, реализующий CollectorInterface, может присоединиться к сканированию. Он получает имя каждого класса и ReflectionClass — делайте своё чтение атрибутов и регистрацию.

src/RouteCollector.php
<?php

use Flytachi\Winter\DI\Contract\CollectorInterface;
use ReflectionClass;

final class RouteCollector implements CollectorInterface
{
  public function __construct(private readonly Router $router) {}

  public function collect(string $class, ReflectionClass $ref): void
  {
      foreach ($ref->getAttributes(Route::class) as $attr) {
          $route = $attr->newInstance();
          $this->router->add($route->method, $route->path, $class);
      }
  }
}

Держите коллекторы лёгкими

collect() выполняется в плотном цикле по потенциально сотням классов. Делайте там только дешёвую работу (чтение атрибутов, регистрация привязки); откладывайте инстанцирование и тяжёлую настройку на время разрешения.

Связанное