Laravel трюки: автоматическое подключение каналов логирования
Всем привет! Меня зовут Иван Шишкин и я руковожу разработкой в агентстве Intensa.
В этой статье хотел бы поделиться методом автоматического подключения каналов логирования в Laravel через механизм сервис контейнеров (DI).
Рассказываю и показываю в статье
Логирование в Laravel
Фреймворк имеет на борту мощный функционал логирования, реализованный на базе библиотеки Monolog и для удобства клиентского кода скрыт за фасадом Log.
В этой статье я не буду углубляться в устройство и возможности функционала логирования, для этого есть прекрасная документация.
Определение проблемы
Обычно для сбора логов в классах мы используем следующую конструкцию:
logger = Log::channel('task');
}
}
Сохраняем экземпляр LoggerInterface конкретного канала логирования в свойство класса.
Такой подход имеет место быть, если:
— у вас небольшой проект,
— логирование введено в класс временно,
— вы не используете DI в проекте.
В остальных случаях такая реализация даст сильную связанность ваших классов с конкретным каналом логирования. И, когда нам понадобиться изменить поведение узла логирования, например, поменять канал логирования, придётся прыгать по классам и переписывать код. При большом количестве классов в проекте это может вызвать у вас боль и страдания.
Автоматическое подключение
Перед тем как начать описание, еще раз подчеркну — в нашем мире мало магии и PHP не исключение. Для того, чтобы автоматическое подключение сработало, классы вашего проекта должны быть получены через DI.
Первым делом нужно создать интерфейс-маркер, к которому мы привяжем реализацию. Интерфейс должен наследовать от LoggerAwareInterface.
Теперь в провайдере свяжем данный интерфейс с нужным каналом логирования при помощи метода afterResolving
.
app->afterResolving(
DefaultLoggerAwareInterface::class,
function (DefaultLoggerAwareInterface $aware) {
// Привязываем канал "common".
// Log::channel вернет экземпляр \Psr\Log\LoggerInterface
$aware->setLogger(Log::channel('common'));
}
);
}
}
Переходим к подключению нашего логгера к классу. Для демонстрации возьму контроллер из предыдущего блока.
Имплементируем интерфейс DefaultLoggerAwareInterface и подключаем трейт Psr\Log\LoggerAwareTrait, чтобы наш класс получил свойство $this->logger
.
Логируем нужные данные в нашем классе:
logger->info('Данные запроса', $request->toArray());
}
}
Для безопасного вызова логгера я бы рекомендовал использовать Null-safe оператор перед вызовом метода логирования. Это исключит фатальные ошибки в вашем коде, в случае, если по какой-то причине логгер не был доставлен в ваш класс.
$this->logger?->info('Данные запроса', $request->toArray());
Без магии
Также есть вариант пробрасывать логгеры в ваши классы через автоматическое связывание (Autowiring).
Передаем аргумент LoggerInterface в метод вашего класса
logger->info('Данные запроса', $request->toArray());
}
}
В AppServiceProvider определим правило для сервис контейнера. Через данную запись мы явно определяем, что если, класс TaskController запросил у DI экземпляр LoggerInterface, то нужно вернуть канал логирования с кодом «common»
app->when(TaskController::class)
->needs(LoggerInterface::class)
->give(fn() => Log::channel('common'));
}
}
Эпилог
В завершение хочу отметить, что описанное в статье не является панацеей и правилом из категории «Right Way». Я бы назвал это трюком, который поможет сделать ваш код менее связанным и поддерживаемым в перспективе.