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.
<?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.
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:
<?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):
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.
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.
<?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.
Related
- Scopes — what each attribute means
- Attributes — the scope-attribute reference
- Service providers — bindings that attributes can’t express
- Resolution lifecycle — what happens on
make()