Package · di

Inject into properties & methods

Constructor injection is the default, but sometimes you can’t use it — a base class owns the constructor, or you want a dependency resolved lazily. Property injection with #[Autowired] / #[Inject] covers that, and call() injects a method’s parameters when you invoke it.

Inject by type with #[Autowired]

Put #[Autowired] on a property and the container resolves it by the property’s declared type after the object is constructed. The property need not be public.

src/SyncCommand.php
<?php

use Flytachi\Winter\DI\Attribute\Autowired;

class SyncCommand extends BaseCommand
{
  #[Autowired]
  private UserService $userService;

  #[Autowired]
  private CacheInterface $cache;

  public function handle(): void
  {
      $this->userService->sync();
  }
}

Property injection runs automatically during make(), right after the constructor. Reach for it when the constructor isn’t yours to change — otherwise prefer constructor parameters.

Interfaces still need a binding

#[Autowired] private CacheInterface $cache resolves only if CacheInterface has a binding — autowiring can’t pick an implementation on its own. Bind it in a provider, or register a contextual() factory for per-consumer instances.

Inject a specific class or named value with #[Inject]

#[Autowired] always resolves by the declared type. #[Inject] overrides that — inject a specific implementation, or a named value that has no meaningful type.

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

class ReportService
{
  // specific implementation, ignoring the global CacheInterface binding
  #[Inject(FileCache::class)]
  private CacheInterface $cache;

  // named scalar registered with $container->set('config.timeout', 30)
  #[Inject('config.timeout')]
  private int $timeout;
}

The same works on constructor parameters — override the wiring for one argument without touching global bindings:

php
class UserService
{
  public function __construct(
      private CacheInterface $primary,                 // global binding → e.g. RedisCache
      #[Inject(FileCache::class)] private CacheInterface $fallback,  // local override
      #[Inject('config.timeout')] private int $timeout,             // named value
  ) {}
}

Autowired vs Inject

#[Autowired] = resolve by the declared type (an explicit marker for by-type injection). #[Inject(X::class)] = inject a specific class. #[Inject('key')] = inject a named value set with set(). A bare #[Inject] with no argument behaves exactly like #[Autowired].

Invoke a method with call()

call() invokes a method or closure and resolves its parameters from the container. This is the integration point for controllers, commands, and jobs — the method declares what it needs and the container supplies it.

php
// [class-string, method] — the class is resolved first, then the method called
$container->call([UserController::class, 'index']);

// [object, method] — use an existing instance
$container->call([$controller, 'store']);

// closure — every typed parameter is resolved
$container->call(fn(UserService $s, AuthContext $a) => $s->current($a->user()));

Pass runtime values the container can’t autowire (ids, sizes, request data) as overrides, keyed by parameter name:

php
class ImportJob
{
  public function run(UserService $service, int $chunkSize): void { /* ... */ }
}

// $service is autowired; $chunkSize comes from the override
$container->call([ImportJob::class, 'run'], ['chunkSize' => 500]);

The same overrides array works on make() for constructor parameters:

php
$job = $container->make(ImportJob::class, ['chunkSize' => 500]);

Overrides bypass the singleton cache

Passing an overrides array always builds a fresh instance — the result is not read from or written to the singleton/request cache, since the overridden object isn’t the “canonical” one. Call make() with no overrides for the shared instance.