Bind interfaces & factories
Autowiring resolves concrete classes on its own, but it can’t guess which implementation an
interface should get, or how to build an object that needs runtime config. A
ServiceProvider is where those bindings live — grouped in one place instead of scattered
across bootstrap.
Create a provider
Extend ServiceProvider and put your bindings in register(). The container is passed in.
<?php
use Flytachi\Winter\DI\Contract\ServiceProvider;
use Flytachi\Winter\DI\Container;
class AppServiceProvider extends ServiceProvider
{
public function register(Container $c): void
{
// interface → implementation
$c->singleton(CacheInterface::class, RedisCache::class);
// factory closure — receives the container
$c->bind(MailerInterface::class, fn(Container $c) =>
new SmtpMailer(
host: env('MAIL_HOST', 'localhost'),
logger: $c->make(LoggerInterface::class),
)
);
// named scalar value
$c->set('config.timeout', (int) env('APP_TIMEOUT', 30));
}
}Register it once at bootstrap. Providers run immediately, in registration order:
Container::init()
->register(AppServiceProvider::class)
->register(DatabaseServiceProvider::class);Bind an interface to an implementation
The most common binding: when a class asks for CacheInterface, give it RedisCache.
$c->singleton(CacheInterface::class, RedisCache::class);
// Now any autowired CacheInterface resolves to a shared RedisCache
class PageRenderer
{
public function __construct(private CacheInterface $cache) {}
}Pick the binding method by the lifetime you want — the scope is baked into the call:
| Method | Scope | Use for |
|---|---|---|
singleton() |
one shared instance | stateless services, connections, repositories |
bind() / transient() |
new each time | stateful objects, builders |
request() |
one per request / coroutine | per-request state |
Full signatures in the API reference; lifetime guidance in Scopes.
Register a factory closure
When construction needs runtime config or custom logic, bind a closure. It receives the container so it can resolve other dependencies.
$c->singleton(Connection::class, fn(Container $c) =>
new PdoConnection(env('DB_DSN'), env('DB_USER'), env('DB_PASS'))
);
$c->bind(ReportBuilder::class, fn(Container $c) =>
new ReportBuilder($c->make(Connection::class), locale: 'en_US')
);Store a named value
Scalars and pre-built instances go in under a string key with set(), then get injected by
name with #[Inject('key')].
$c->set('config.timeout', 30);
$c->set('db.connection', $existingPdoInstance);
class ApiClient
{
public function __construct(
#[Inject('config.timeout')] private int $timeout,
) {}
}Split providers by domain
Group bindings by concern — one provider per subsystem keeps bootstrap readable.
class DatabaseServiceProvider extends ServiceProvider
{
public function register(Container $c): void
{
$c->singleton(Connection::class, fn() =>
new PdoConnection(env('DB_DSN'), env('DB_USER'), env('DB_PASS'))
);
$c->singleton(UserRepository::class);
$c->singleton(OrderRepository::class);
}
}
class AuthServiceProvider extends ServiceProvider
{
public function register(Container $c): void
{
$c->request(AuthContext::class);
$c->singleton(TokenValidator::class, JwtTokenValidator::class);
$c->set('auth.secret', env('JWT_SECRET'));
}
}Providers vs attributes
Use a scope attribute (#[Singleton] …) when a concrete class owns its own lifetime. Use a
provider for interface → implementation mappings, factories with runtime config, and
named values — things a concrete class can’t declare about itself.
Related
- Injecting into properties —
#[Autowired]and#[Inject] - Per-consumer logger —
contextual()factories - Scopes — choosing a lifetime
- API reference —
bind/singleton/transient/request/set