Package · di

Attributes

The complete attribute surface. Scope attributes go on classes; injection attributes go on properties and constructor parameters. All are final readonly.

At a glance

Attribute Target Argument Purpose
#[Singleton] class One shared instance per process.
#[Request] class One instance per request / coroutine.
#[Transient] class A new instance every resolution (the default).
#[Autowired] property, parameter Inject by the declared type.
#[Inject] property, parameter ?string $id = null Inject a specific class or named value.
#[Lazy] property, parameter Inject a deferred proxy, resolved on first use.

Namespace: Flytachi\Winter\DI\Attribute\.

Scope attributes

Set the lifetime of a class. Read by DICollector during a scan, or by the container on first make() when the class isn’t manually bound. A manual binding always overrides the attribute. Full semantics on the Scopes page.

#[Singleton]

php
use Flytachi\Winter\DI\Attribute\Singleton;

#[Singleton]
class UserRepository
{
  public function __construct(private DatabaseConnection $db) {}
}

One instance per container (process) lifetime. Equivalent to $container->singleton(UserRepository::class). Use for stateless services. Not for per-request state under Swoole — use #[Request].

#[Request]

php
use Flytachi\Winter\DI\Attribute\Request;

#[Request]
class AuthContext
{
  private ?User $user = null;
}

One instance per HTTP request / coroutine. Under Swoole it is isolated via Coroutine::getContext(); under FPM / CLI it is equivalent to #[Singleton] (one process = one request). Use for per-request state.

#[Transient]

php
use Flytachi\Winter\DI\Attribute\Transient;

#[Transient]
class QueryBuilder
{
  private array $clauses = [];
}

A new instance on every make() / injection. This is already the default when no attribute is present — #[Transient] just makes the intent explicit.

Injection attributes

Mark where and how a dependency is supplied. On a property, injection runs automatically after the constructor during make(); the property need not be public.

#[Autowired]

php
use Flytachi\Winter\DI\Attribute\Autowired;

class UserController
{
  #[Autowired]
  private UserService $service;

  public function __construct(
      #[Autowired] private CacheInterface $cache,
  ) {}
}

Resolves the dependency by the declared PHP type. Equivalent to #[Inject] with no argument — an explicit marker for by-type injection. An interface still needs a binding for this to resolve.

#[Inject]

php
use Flytachi\Winter\DI\Attribute\Inject;

// #[Inject(?string $id = null)]

Overrides autowiring for one parameter or property.

$id value Behaviour
null (default) Resolve by the declared PHP type — same as #[Autowired].
FQCN string, e.g. #[Inject(FileCache::class)] Inject that specific class, bypassing the global binding for the declared type.
named key, e.g. #[Inject('config.timeout')] Inject a scalar / pre-built value registered with Container::set().
php
class UserService
{
  public function __construct(
      private CacheInterface $primary,                 // global binding
      #[Inject(FileCache::class)] private CacheInterface $fallback,  // specific class
      #[Inject('config.timeout')] private int $timeout,             // named value
  ) {}
}

#[Lazy]

php
use Flytachi\Winter\DI\Attribute\Lazy;

class SmsSendService
{
  #[Lazy]
  private FakeSendService $peer;      // native PHP 8.4 proxy; resolved on first use
}

Injects a native PHP 8.4 lazy proxy (ReflectionClass::newLazyProxy()) instead of resolving immediately. The real instance is built from the container on first access — the mechanism for breaking circular dependencies.

  • Works on properties and constructor parameters; combinable with #[Autowired] / #[Inject].
  • The proxied type must be a concrete class. For an interface, name the concrete: #[Inject(SmsSendService::class), Lazy]. A bare #[Lazy] on an interface or abstract class throws ContainerException.
  • The proxy is type-compatible with the real class and stays uninitialised until first use.

See Lazy proxies for the internals.

Precedence

For a single dependency, the container applies the first rule that matches:

  1. A make() / call() override keyed by the parameter name.
  2. #[Inject] — explicit $id, or the declared type when $id is null.
  3. #[Autowired] — the declared type.
  4. Autowiring — the declared type, with the parameter’s default value as a fallback if the type can’t be resolved and a default exists.

#[Lazy] is orthogonal: it changes when the chosen dependency is built (deferred to first use), not which one.