flytachi / winter-di
Lightweight PSR-11 DI container for Winter framework (autowiring, scopes, attributes)
Package info
pkg:composer/flytachi/winter-di
Requires
- php: >=8.4
- psr/container: ^2.0
Requires (Dev)
- phpunit/phpunit: @stable
- squizlabs/php_codesniffer: @stable
- swoole/ide-helper: ^6.0
Suggests
- ext-swoole: Required for request scope coroutine isolation in Swoole runtime
README
A PSR-11 dependency injection container for PHP 8.4+: you declare what a class needs as typed parameters, and the container builds the whole object graph โ resolving every dependency by type, recursively, with the lifetime you asked for.
One runtime dependency (psr/container), no configuration files. Built for the Winter
framework, usable in any PSR-11 consumer, and safe in a long-lived Swoole worker where one
process serves many requests at once.
๐ Documentation ยท Quick start ยท API reference
Installation
composer require flytachi/winter-di
Requires PHP 8.4+ and psr/container ^2.0. ext-swoole is optional โ it only matters for
per-coroutine isolation of the request scope.
Quick start
use Flytachi\Winter\DI\Container; use Flytachi\Winter\DI\Scanner; use Flytachi\Winter\DI\Collector\DICollector; // bootstrap.php โ once at application start $container = Container::init(); Scanner::run(__DIR__ . '/src', cache: __DIR__ . '/var/cache/di.php') ->collect(new DICollector($container)) // auto-register #[Singleton], #[Request], #[Transient] ->execute(); $container->register(AppServiceProvider::class); // bind interfaces and factories // Resolve anywhere $service = Container::getInstance()->make(UserService::class); // Call a method with everything injected $result = Container::getInstance()->call([UserController::class, 'index']);
Nothing else is required: a class with typed constructor parameters resolves without being registered at all.
What you get
- Autowiring by type โ constructor parameters resolved recursively; you stop writing
newfor services. - Three explicit lifetimes โ
singleton,transient,request, chosen by attribute or binding. - Attributes, not config โ
#[Singleton],#[Autowired],#[Inject],#[Lazy]live on the class that they describe. - Property injection โ including private properties declared in a parent class.
- Contextual factories โ
contextual()receives the consuming class, which is how a logger can name its channel after whoever injected it. - Lazy proxies โ
#[Lazy]injects a native PHP 8.4 proxy resolved on first use, the escape hatch for a circular dependency. - One scan for everything โ
Scannerwalks the tree once and feeds every collector; tokenised, so a class is found whatever its formatting, with an optional production cache. - Swoole-safe โ request scope and the resolution stack live in the coroutine's context; concurrent resolution never invents a circular dependency, and a singleton is built once even when a cold worker is hit by many requests at once.
Scopes in one table
| Scope | Lifetime | Concurrent make() of the same class |
|---|---|---|
singleton |
one instance per worker process | first builds it, the rest wait and get it |
transient |
new instance every time (default) | each builds its own |
request |
one per request / coroutine | each coroutine has its own |
One rule decides every combination:
A class may hold a reference to a shorter-lived object only if it does not outlive it.
Injected properties are resolved once, when the holder is built. A #[Singleton] holding a
#[Request] bean therefore freezes the first request's instance for the worker's lifetime โ
silently, and transitively through any number of intermediate classes. With an authentication
context in that position, every user after the first is served under the first user's identity.
See Scopes.
A taste of the API
// Registration $c->bind(CacheInterface::class, RedisCache::class); // transient $c->singleton(CacheInterface::class, RedisCache::class); // one per process $c->request(AuthContext::class); // one per request / coroutine $c->set('config.timeout', 30); // named value $c->bind(MailerInterface::class, fn(Container $c) => // factory closure new SmtpMailer(env('MAIL_HOST'), $c->make(LoggerInterface::class))); $c->contextual(LoggerInterface::class, // consumer-aware factory fn(Container $c, ?string $consumer) => LoggerFactory::getLogger($consumer ?? 'app')); // Resolution $c->make(UserService::class); $c->make(ImportJob::class, ['chunkSize' => 500]); // overrides โ always built fresh $c->call(fn(UserService $s) => $s->all()); // method / closure injection // Injection without construction โ the caller keeps the object's identity $c->inject($repository); // Ending a request scope where nothing ends it for you (a worker loop) $c->flushRequestScope();
#[Singleton] class UserRepository {} class SomeCommand { #[Autowired] // by declared type private UserService $service; #[Inject('config.timeout')] // named value private int $timeout; #[Lazy] // proxy now, resolved on first use private ReportBuilder $reports; }
Full signatures, defaults and edge cases: API reference.
Documentation
The user-facing documentation lives at winterframe.net/packages/di (the link picks your language; RU and EN are both complete).
Start here
| Page | What it answers |
|---|---|
| Introduction | What the container is and whether you need it |
| Installation | Requirements, install, optional ext-swoole |
| Quick start | Bootstrap, auto-discovery, first resolved service |
| Mental model | How to think about it โ and why the default is transient |
Guides
| Page | What it answers |
|---|---|
| Service providers | Grouping bindings, interface โ implementation, factories |
| Scanning & auto-discovery | Registering classes without touching bootstrap; the scan cache |
| Injecting into properties | #[Autowired] vs #[Inject], inherited private properties |
| Per-consumer logger | The classic contextual() recipe, end to end |
| Breaking circular dependencies | Reading the error chain, and where to put #[Lazy] |
Reference
| Page | What it answers |
|---|---|
| API reference | Every method, contract and exception |
| Attributes | All six attributes with their targets |
| Scopes | Lifetimes, precedence, the Swoole safety matrix |
Deep dive
| Page | What it answers |
|---|---|
| Resolution lifecycle | What make() does, step by step |
| Request scope & Swoole | Per-coroutine isolation, and the FPM/CLI fallback |
| Concurrent resolution | Many requests, one container โ stacks and singleton waiting |
| Reflection cache | What is memoised per process, and the injection plan |
| Lazy proxies | Deferred resolution, and what a proxy cannot do |
Classes in this package carry an @link to their page, so the same documentation is one click
away from your IDE.
Contributing
Internal technical notes โ exact contracts, invariants, and the reasoning behind decisions that
are not obvious from the code โ live in docs/. Read that before changing
resolution behaviour.
composer test # phpunit composer test-detail # phpunit --testdox composer cs-check # phpcs composer cs-fix # phpcbf
License
MIT License. See LICENSE.