Package · di

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:

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

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

php
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 direct make(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 plain bind()/singleton() exist for the same abstract, injection uses the contextual one; a direct make() 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 — LoggerFactory already 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.