flytachi/winter-di

Lightweight PSR-11 DI container for Winter framework (autowiring, scopes, attributes)

Maintainers

Package info

github.com/Flytachi/winter-di

Homepage

Issues

Documentation

pkg:composer/flytachi/winter-di

Transparency log

Statistics

Installs: 395

Dependents: 2

Suggesters: 0

Stars: 0

v2.2.0 2026-08-20 09:13 UTC

This package is auto-updated.

Last update: 2026-08-20 09:13:51 UTC


README

Latest Version on Packagist PHP Version Require Software License

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 new for 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 โ€” Scanner walks 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.