Package · di

Auto-register annotated classes

Marking a class #[Singleton] declares its scope, but something has to read that attribute and register the binding. Scanner walks your project tree once and hands every class to a DICollector, which registers the ones that carry a scope attribute — so you don’t list them by hand.

The basic scan

Point Scanner at your source root, attach a DICollector bound to your container, and run.

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 maps each scope attribute to the matching binding:

Attribute Registration
#[Singleton] $container->singleton($class)
#[Request] $container->request($class)
#[Transient] $container->transient($class)

A class with no scope attribute is skipped by the collector — it stays autowirable on demand as transient.

What the Scanner skips

Abstract classes, interfaces, and traits are filtered out before collectors run, so collect() only ever sees instantiable classes. vendor/ is always excluded.

Add a production cache

Scanning the filesystem on every boot is wasteful in production. Pass a cache path: the first boot walks the tree and writes the list of discovered class names; later boots load that list and skip the walk entirely.

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

The cache file is a plain PHP array of fully-qualified class names:

var/cache/di.php
<?php

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

Cache invalidation

The cache stores the class list, not the bindings. Adding, renaming, or moving a class means the list is stale — delete the cache file to force a full rescan on the next boot. Wire this into your deploy step.

Exclude directories

vendor/ is always excluded. Add more paths with exclude() (absolute paths):

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

Run several collectors in one pass

DICollector is one collector; a framework typically has more (routes, exception handlers, console commands). Register them all on the same Scanner and they share a single filesystem walk — no class is read twice.

php
Scanner::run(__DIR__ . '/src', cache: $cachePath)
  ->collect(new DICollector($container))     // scope attributes
  ->collect(new MappingCollector($router))   // route attributes
  ->collect(new ExceptionCollector())        // exception handlers
  ->execute();

Collectors run in registration order for each class.

Write a custom collector

Any class implementing CollectorInterface can join the scan. It receives each class name and a ReflectionClass — do your own attribute reading and registration.

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);
      }
  }
}

Keep collectors lightweight

collect() runs in a tight loop over potentially hundreds of classes. Do only cheap work there (read attributes, register a binding); defer instantiation and heavy setup to resolution time.