Логгер на потребителя
Обычная привязка выдаёт каждому потребителю один и тот же экземпляр. Фабрика contextual()
иная: она также получает класс, в который внедряется зависимость, поэтому может собрать
подстроенный экземпляр под каждого потребителя. Хрестоматийный случай — логгер, называющий
себя по классу, который его использует, — без getLogger(self::class) где бы то ни было.
Проблема
Вы хотите, чтобы каждый класс логировал в свой канал, но не хотите, чтобы каждый класс повторял:
class MainController
{
private LoggerInterface $logger;
public function __construct()
{
$this->logger = LoggerFactory::getLogger(self::class); // boilerplate в каждом классе
}
}Обычный bind(LoggerInterface::class, …) тут не поможет — он не знает, кто просит, поэтому
каждый потребитель получил бы один и тот же канал.
Решение — контекстная фабрика
Зарегистрируйте LoggerInterface через contextual(). Фабрика получает контейнер и имя
класса-потребителя, поэтому может назвать логгер соответственно. Сделайте это один раз — в
провайдере или при 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'),
);
}
}Теперь любой класс получает свой канал, просто запрашивая интерфейс по типу:
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()снова для того же абстракта (например, чтобы заменить дефолт фреймворка), и побеждает более поздняя фабрика.
Не только для логгеров
Под этот паттерн подходит любая зависимость, которая должна подстраиваться под потребителя — сборщик метрик с тегом по имени класса, читатель фича-флагов на модуль, кэш с ключом по потребителю. Логгер — лишь самый наглядный пример.
Связанное
- Сервис-провайдеры — где обычно живёт
contextual() - Внедрение в свойства —
#[Autowired]запускает фабрику - Справочник API —
contextual()иmakeContextual()