Package · di

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.

src/AppServiceProvider.php
<?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:

php
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.

php
$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.

php
$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')].

php
$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.

php
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.