Per-consumer logger
A regular binding gives every consumer the same instance. A contextual() factory is
different: it also receives the class the dependency is being injected into, so it can build a
tailored instance per consumer. The textbook case is a logger that names itself after the
class that uses it — no getLogger(self::class) boilerplate anywhere.
The problem
You want each class to log under its own channel, but you don’t want every class to repeat:
class MainController
{
private LoggerInterface $logger;
public function __construct()
{
$this->logger = LoggerFactory::getLogger(self::class); // boilerplate in every class
}
}A plain bind(LoggerInterface::class, …) can’t help — it has no idea who is asking, so every
consumer would get the same channel.
The fix — a contextual factory
Register LoggerInterface with contextual(). The factory receives the container and the
consumer’s class name, so it can name the logger accordingly. Do it once, in a provider or at
bootstrap.
<?php
use Psr\Log\LoggerInterface;
use Flytachi\Winter\DI\Contract\ServiceProvider;
use Flytachi\Winter\DI\Container;
class LoggingServiceProvider extends ServiceProvider
{
public function register(Container $c): void
{
$c->contextual(
LoggerInterface::class,
fn(Container $c, ?string $consumer) => LoggerFactory::getLogger($consumer ?? 'app'),
);
}
}Now any class gets its own channel just by asking for the interface by type:
class MainController
{
#[Autowired]
private LoggerInterface $logger; // → LoggerFactory::getLogger(MainController::class)
public function index(): void
{
$this->logger->info('hit'); // logged under channel "MainController"
}
}The $consumer argument is the consuming class’s fully-qualified name — or null when there
is no owning class, such as a free closure passed to call(fn(LoggerInterface $l) => …).
How it behaves
A contextual() factory is an overlay on top of your normal bindings, with a few rules
worth knowing:
- Injection only. It applies during constructor, method (
call()), and property injection — the places where a consumer exists. A directmake(LoggerInterface::class)/get()ignores it and uses the regular binding (or resolves the class normally). - Takes precedence at injection time. If both a
contextual()factory and a plainbind()/singleton()exist for the same abstract, injection uses the contextual one; a directmake()uses the plain one. They coexist. - Never cached by the container. The factory runs on every injection. If building is
expensive, cache inside the factory —
LoggerFactoryalready caches per channel, so this stays cheap even in a long-running Swoole worker. - Override by re-registering. Call
contextual()again for the same abstract (e.g. to replace a framework default) and the later factory wins.
Not just for loggers
Any dependency that should adapt to its consumer fits this pattern — a metrics collector tagged with the class name, a feature-flag reader scoped per module, a cache keyed by consumer. The logger is just the clearest example.
Related
- Service providers — where
contextual()usually lives - Injecting into properties —
#[Autowired]triggers the factory - API reference —
contextual()andmakeContextual()