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.
<?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.
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:
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.
// [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:
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:
$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.
Related
- Breaking circular dependencies — add
#[Lazy] - Per-consumer logger — a type-based injection that adapts to the consumer
- Attributes — the full attribute reference
- API reference —
make()/call()and their overrides