Пакет · di

Логгер на потребителя

Обычная привязка выдаёт каждому потребителю один и тот же экземпляр. Фабрика contextual() иная: она также получает класс, в который внедряется зависимость, поэтому может собрать подстроенный экземпляр под каждого потребителя. Хрестоматийный случай — логгер, называющий себя по классу, который его использует, — без getLogger(self::class) где бы то ни было.

Проблема

Вы хотите, чтобы каждый класс логировал в свой канал, но не хотите, чтобы каждый класс повторял:

php
class MainController
{
  private LoggerInterface $logger;

  public function __construct()
  {
      $this->logger = LoggerFactory::getLogger(self::class);   // boilerplate в каждом классе
  }
}

Обычный bind(LoggerInterface::class, …) тут не поможет — он не знает, кто просит, поэтому каждый потребитель получил бы один и тот же канал.

Решение — контекстная фабрика

Зарегистрируйте LoggerInterface через contextual(). Фабрика получает контейнер и имя класса-потребителя, поэтому может назвать логгер соответственно. Сделайте это один раз — в провайдере или при 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'),
      );
  }
}

Теперь любой класс получает свой канал, просто запрашивая интерфейс по типу:

php
class MainController
{
  #[Autowired]
  private LoggerInterface $logger;         // → LoggerFactory::getLogger(MainController::class)

  public function index(): void
  {
      $this->logger->info('hit');           // залогировано в канал "MainController"
  }
}

Аргумент $consumer — полностью квалифицированное имя класса-потребителя — или null, когда владеющего класса нет, например у свободного замыкания, переданного в call(fn(LoggerInterface $l) => …).

Как это ведёт себя

Фабрика contextual() — это наложение поверх обычных привязок, с несколькими правилами, которые стоит знать:

  • Только при внедрении. Она применяется во время внедрения в конструктор, метод (call()) и свойство — там, где есть потребитель. Прямой make(LoggerInterface::class) / get() игнорирует её и использует обычную привязку (или разрешает класс как обычно).
  • Имеет приоритет при внедрении. Если для одного абстракта существуют и фабрика contextual(), и обычный bind()/singleton(), при внедрении используется контекстная; прямой make() использует обычную. Они сосуществуют.
  • Никогда не кэшируется контейнером. Фабрика выполняется при каждом внедрении. Если построение дорогое, кэшируйте внутри фабрики — LoggerFactory уже кэширует по каналу, так что это дёшево даже в долгоживущем Swoole-воркере.
  • Переопределение — повторной регистрацией. Вызовите contextual() снова для того же абстракта (например, чтобы заменить дефолт фреймворка), и побеждает более поздняя фабрика.

Не только для логгеров

Под этот паттерн подходит любая зависимость, которая должна подстраиваться под потребителя — сборщик метрик с тегом по имени класса, читатель фича-флагов на модуль, кэш с ключом по потребителю. Логгер — лишь самый наглядный пример.

Связанное