Если вы видите это, значит, я еще не придумал, что написать.
При разработке проектов я придерживаюсь принципа подробного логирования критически важных частей приложения. Это позволяет по логам понять, что происходило, что пошло не так и почему.
Однако при активном использовании логирования в проектах на Laravel я регулярно сталкивался с рядом неудобств:
Чтобы решить эти (и не только) проблемы, я сначала добавил необходимые улучшения в одном из проектов, затем начал переносить их в другие, а в итоге оформил всё в отдельный пакет — faustoff/laravel-contextify. Им я и хочу с вами поделиться.
Контекстное логирование с встроенными уведомлениями для Laravel.
use Faustoff\Contextify\Facades\Contextify;
Contextify::notice('Updated', ['key' => 'value'])->notify(['mail']);
// [2025-01-01 12:00:00] local.NOTICE: Updated {"key":"value"} {"trace_id":"4f9c2a1b"}
Laravel Contextify расширяет возможности логирования Laravel двумя основными (но не единственными) функциями:
Предоставляет фасад Contextify, совместимый с фасадом Log от Laravel: те же методы (debug, info, notice, warning, error, critical, alert, emergency) с идентичными параметрами, плюс цепочный метод notify().
Происхождение названия: “Contextify” объединяет Context (контекст) и Notify (уведомлять), отражая двойное назначение — обогащать логи контекстными данными и отправлять уведомления о событиях логирования.
only и exceptУстановите пакет через Composer:
composer require faustoff/laravel-contextify
При необходимости опубликуйте файл конфигурации:
php artisan vendor:publish --tag=contextify-config
Это создаст config/contextify.php для настройки провайдеров контекста и уведомлений.
Добавьте в .env для настройки получателей уведомлений:
CONTEXTIFY_MAIL_ADDRESSES=admin@example.com,team@example.com
CONTEXTIFY_TELEGRAM_CHAT_ID=123456789
Примечание: Для уведомлений Telegram требуется установка пакета laravel-notification-channels/telegram вручную.
Используйте фасад Contextify так же, как фасад Log от Laravel. Логи автоматически включают дополнительный контекст из провайдеров контекста, настроенных для логирования:
<?php
use Faustoff\Contextify\Facades\Contextify;
Contextify::debug('Отладочное сообщение', ['key' => 'value']);
// [2025-01-01 12:00:00] local.DEBUG: Отладочное сообщение {"key":"value"} {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Services/ExampleService.php:42","class":"App\\Services\\ExampleService"}
Contextify::info('Пользователь вошёл в систему', ['user_id' => 123]);
// [2025-01-01 12:00:00] local.INFO: Пользователь вошёл в систему {"user_id":123} {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Http/Controllers/Auth/LoginController.php:55","class":"App\\Http\\Controllers\\Auth\\LoginController"}
Contextify::notice('Важное уведомление');
// [2025-01-01 12:00:00] local.NOTICE: Важное уведомление {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"routes/web.php:10","class":null}
// ... и то же самое для warning, error, critical, alert и emergency
Цепочкой вызовите notify() после любого метода логирования для отправки уведомлений. Уведомления включают сообщение лога, контекст и дополнительный контекст из провайдеров контекста, настроенных для уведомлений.
Фильтруйте каналы с помощью параметров only и except:
<?php
use Faustoff\Contextify\Facades\Contextify;
Contextify::error('Ошибка обработки платежа', ['order_id' => 456])->notify();
// [2025-01-01 12:00:00] local.ERROR: Ошибка обработки платежа {"order_id":456} {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Http/Controllers/Api/OrderController.php:133","class":"App\\Http\\Controllers\\Api\\OrderController"}
// Уведомление с контекстом {"order_id":456} и дополнительным контекстом отправлено во все настроенные каналы уведомлений
Contextify::critical('Потеряно соединение с базой данных')->notify(only: ['mail']);
// [2025-01-01 12:00:00] local.CRITICAL: Потеряно соединение с базой данных {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/MonitorCommand.php:71","class":"App\\Console\\Commands\\MonitorCommand"}
// Уведомление с дополнительным контекстом отправлено только в канал почты
Contextify::alert('Обнаружена попытка взлома')->notify(except: ['telegram']);
// [2025-01-01 12:00:00] local.ALERT: Обнаружена попытка взлома {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Providers/AppServiceProvider.php:25","class":"App\\Providers\\AppServiceProvider"}
// Уведомление с дополнительным контекстом отправлено во все настроенные каналы уведомлений, кроме канала Telegram
По необходимости, вы можете переопределить стандартную реализацию уведомления LogNotification:
<?php
namespace App\Notifications;
use Faustoff\Contextify\Notifications\LogNotification;
class CustomLogNotification extends LogNotification
{
// Переопределите методы или добавьте новые
}
Обновите конфигурацию:
'notifications' => [
'class' => \App\Notifications\CustomLogNotification::class,
// ... другие настройки уведомлений
],
Уведомления об исключениях отправляются автоматически (включено по умолчанию). Уведомления включают детали исключения (сообщение и трассировку стека) и дополнительный контекст из провайдеров контекста, настроенных для уведомлений.
По необходимости, вы можете переопределить стандартную реализацию уведомления ExceptionNotification:
<?php
namespace App\Notifications;
use Faustoff\Contextify\Notifications\ExceptionNotification;
class CustomExceptionNotification extends ExceptionNotification
{
// Переопределите методы или добавьте новые
}
Обновите конфигурацию:
'notifications' => [
'exception_class' => \App\Notifications\CustomExceptionNotification::class,
// ... другие настройки уведомлений
],
Чтобы отключить автоматические уведомления об исключениях, установите reportable в null:
'notifications' => [
'reportable' => null,
// ... другие настройки уведомлений
],
Примечание:
ExceptionNotificationFailedExceptionпредотвращает бесконечные циклы при сбое уведомлений об исключениях.
Провайдеры контекста добавляют дополнительные контекстные данные в логи и уведомления, помогая вам сохранять сообщения в записях логов и уведомлениях короткими и чистыми, перемещая дополнительный контекст из самого сообщения в отдельную область. Контекстные данные по-прежнему присутствуют в записи лога или уведомлении, но они отделены от самого сообщения — оставляя сообщение в центре внимания, при этом сохраняя все дополнительные контекстные данные для поиска и анализа. Вам больше не нужно заботиться о том, чтобы каждый раз явно передавать нужный дополнительный контекст, так как он будет добавлен автоматически.
Статические провайдеры возвращают данные, которые остаются постоянными на протяжении жизненного цикла запроса/процесса. Они реализуют StaticContextProviderInterface.
Встроенные:
pid)trace_id) для распределённой трассировкиhostname)environment)Статический контекст кэшируется при загрузке приложения. Используйте touch() для ручного обновления, что полезно при форке процесса (например, для воркеров очередей) для генерации нового ID трассировки:
<?php
use Faustoff\Contextify\Facades\Contextify;
use Faustoff\Contextify\Context\Providers\TraceIdContextProvider;
// Обновить конкретный провайдер (например, сгенерировать новый ID трассировки)
Contextify::touch(TraceIdContextProvider::class);
// Обновить все статические провайдеры
Contextify::touch();
Динамические провайдеры обновляют данные при каждом вызове логирования. Они реализуют DynamicContextProviderInterface.
Встроенные:
file) и имя класса (class) вызывающего кодаpeak_memory_usage)datetime)Реализуйте StaticContextProviderInterface или DynamicContextProviderInterface:
<?php
namespace App\Context\Providers;
use Faustoff\Contextify\Context\Contracts\StaticContextProviderInterface;
class CustomContextProvider implements StaticContextProviderInterface
{
public function getContext(): array
{
return [
// реализация ...
];
}
}
Добавьте пользовательские провайдеры в config/contextify.php:
<?php
use App\Context\Providers\CustomContextProvider;
use Faustoff\Contextify\Context\Providers\CallContextProvider;
use Faustoff\Contextify\Context\Providers\EnvironmentContextProvider;
use Faustoff\Contextify\Context\Providers\HostnameContextProvider;
use Faustoff\Contextify\Context\Providers\ProcessIdContextProvider;
use Faustoff\Contextify\Context\Providers\TraceIdContextProvider;
return [
'logs' => [
'providers' => [
// Встроенные провайдеры
ProcessIdContextProvider::class,
TraceIdContextProvider::class,
CallContextProvider::class,
// Пользовательские провайдеры
CustomContextProvider::class,
],
// ... другие настройки логов
],
'notifications' => [
'providers' => [
// Встроенные провайдеры
HostnameContextProvider::class,
ProcessIdContextProvider::class,
TraceIdContextProvider::class,
EnvironmentContextProvider::class,
CallContextProvider::class,
// Пользовательские провайдеры
CustomContextProvider::class,
],
// ... другие настройки уведомлений
],
];
Определите отдельные провайдеры контекста для логов и уведомлений. Если провайдер присутствует в обоих наборах, те же контекстные данные используются для обоих случаев.
Настройте в config/contextify.php:
logs.providers — провайдеры для записей логовnotifications.providers — провайдеры для уведомленийПример:
<?php
use Faustoff\Contextify\Context\Providers\CallContextProvider;
use Faustoff\Contextify\Context\Providers\EnvironmentContextProvider;
use Faustoff\Contextify\Context\Providers\HostnameContextProvider;
use Faustoff\Contextify\Context\Providers\PeakMemoryUsageContextProvider;
use Faustoff\Contextify\Context\Providers\ProcessIdContextProvider;
use Faustoff\Contextify\Context\Providers\TraceIdContextProvider;
return [
'logs' => [
'providers' => [
ProcessIdContextProvider::class, // Общий
TraceIdContextProvider::class, // Общий
CallContextProvider::class, // Только для логов
PeakMemoryUsageContextProvider::class, // Только для логов
],
// ... другие настройки логов
],
'notifications' => [
'providers' => [
HostnameContextProvider::class, // Только для уведомлений
EnvironmentContextProvider::class, // Только для уведомлений
ProcessIdContextProvider::class, // Общий
TraceIdContextProvider::class, // Общий
],
// ... другие настройки уведомлений
],
];
Поддерживаются каналы mail и telegram из коробки. Почта работает сразу; для Telegram требуется пакет laravel-notification-channels/telegram.
Настройте каналы в config/contextify.php:
'notifications' => [
/*
* Используйте формат ассоциативного массива ['channel' => 'queue'] для указания
* очереди для каждого канала. При простом массиве ['channel'] будет использоваться очередь 'default'.
*/
'channels' => [
'mail' => 'mail-queue',
'telegram' => 'telegram-queue',
],
'mail_addresses' => explode(',', env('CONTEXTIFY_MAIL_ADDRESSES', '')),
// ... другие настройки уведомлений
],
Например, чтобы добавить уведомления Slack, необходимо:
toSlack() согласно документации:<?php
namespace App\Notifications;
use Faustoff\Contextify\Notifications\LogNotification;
use Illuminate\Notifications\Messages\SlackMessage;
class CustomLogNotification extends LogNotification
{
public function toSlack($notifiable): SlackMessage
{
// См. https://laravel.com/docs/12.x/notifications#formatting-slack-notifications
return (new SlackMessage())
->content(ucfirst($this->level) . ': ' . $this->message);
}
}
routeNotificationForSlack() согласно документации:<?php
namespace App\Notifications;
use Faustoff\Contextify\Notifications\Notifiable;
class CustomNotifiable extends Notifiable
{
public function routeNotificationForSlack($notification): string
{
// См. https://laravel.com/docs/12.x/notifications#routing-slack-notifications
return config('services.slack.notifications.channel');
}
}
Настроить Slack в config/services.php.
Обновить config/contextify.php:
'notifications' => [
'class' => \App\Notifications\CustomLogNotification::class,
'notifiable' => \App\Notifications\CustomNotifiable::class,
'channels' => [
'mail',
'telegram',
'slack'
],
// ... другие настройки уведомлений
],
Примечание: Для уведомлений об исключениях расширьте
ExceptionNotificationи добавьте методtoSlack()аналогичным образом.
Нужно больше каналов уведомлений? Добро пожаловать на Laravel Notifications Channels.
Используйте трейт Faustoff\Contextify\Console\Trackable для логирования начала, завершения и времени выполнения команды:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Faustoff\Contextify\Console\Trackable;
use Faustoff\Contextify\Facades\Contextify;
class SyncData extends Command
{
use Trackable;
protected $signature = 'data:sync';
public function handle(): int
{
// Ваша бизнес-логика здесь
Contextify::notice('Данные синхронизированы');
return self::SUCCESS;
}
}
Лог:
[2025-01-01 12:00:00] local.DEBUG: Run with arguments {"command":"data:sync"} {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/SyncData.php:42","class":"App\\Console\\Commands\\SyncData"}
[2025-01-01 12:00:00] local.NOTICE: Данные синхронизированы {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/SyncData.php:42","class":"App\\Console\\Commands\\SyncData"}
[2025-01-01 12:00:00] local.DEBUG: Execution time: 1 second {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/SyncData.php:42","class":"App\\Console\\Commands\\SyncData"}
Используйте трейт Faustoff\Contextify\Console\Outputable для перехвата вывода консоли Laravel из методов типа info() и сохранения его в логах:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Faustoff\Contextify\Console\Outputable;
class SyncData extends Command
{
use Outputable;
protected $signature = 'data:sync';
public function handle(): int
{
// Ваша бизнес-логика здесь
$this->info('Данные синхронизированы');
return self::SUCCESS;
}
}
Лог:
[2025-01-01 12:00:00] local.NOTICE: Данные синхронизированы {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/SyncData.php:42","class":"App\\Console\\Commands\\SyncData"}
Обрабатывайте сигналы завершения (SIGQUIT, SIGINT, SIGTERM по умолчанию) для корректного завершения. Используйте соответствующий трейт с SignalableCommandInterface:
TerminatableV62 для symfony/console:<6.3 (Laravel 9, 10)TerminatableV63 для symfony/console:^6.3 (Laravel 9, 10)TerminatableV70 для symfony/console:^7.0 (Laravel 11+)<?php
namespace App\Console\Commands;
use Faustoff\Contextify\Console\TerminatableV62;
use Illuminate\Console\Command;
use Symfony\Component\Console\Command\SignalableCommandInterface;
class ConsumeStats extends Command implements SignalableCommandInterface
{
use TerminatableV62;
protected $signature = 'stats:consume';
public function handle(): void
{
while (true) {
// ...
if ($this->shouldTerminate) {
// Выполнение прервано обработчиком сигнала завершения
break;
}
}
}
}
Лог:
[2025-01-01 12:00:00] local.WARNING: Received SIGTERM (15) shutdown signal {"pid":12345,"trace_id":"4f9c2a1bd3e7a8f0","file":"app/Console/Commands/ConsumeStats.php:42","class":"App\\Console\\Commands\\ConsumeStats"}
Если вы видите это, значит, я еще не придумал, что написать.
Сервисный слой часто разрастается до «толстых» классов, где сложно поддерживать код и переиспользовать отдельные части. В каком-то году по воле случая я смотрел ютубчик и наткнулся на подход Laravel Actions, основная суть заключается в создании простых классов, каждый из которых выполняет одну конкретную задачу (одно действие). Подход мне в целом понравился. Однако со временем, основной пакет стал обрастать фичами контекста, экшены как контроллер, как листенер, как консольная команда и т п, экшены стали размывать свою ответсвенность и хоть это и опционально, но я стал замечать, что во многих проектах это уже стало своего рода стандартом когда в один объект напихивают ответсвнности за все слои приложения.
Мне пришла идея создать пакет простых действий с решеним рутинных операций таких как “транзакции”, “кеширование”, “мемонизация”, “События”, “DIP”. А так же внедрить сценарный подход, когда есть объекты которые агрегируют простые и существующие действия в некий сценарий UseCase.
Так пришло начало Simple Actions – атомарные Actions и сценарные UseCases. Пакет: lemax10/simple-actions (GitHub).
use LeMaX10\SimpleActions\Action;
class CreateUserAction extends Action
{
protected function handle(string $name, string $email): \App\Models\User
{
return \App\Models\User::create(compact('name', 'email'));
}
}
// Запуск
$user = CreateUserAction::make()->run('John', 'john@example.com');
// или хелпером
$user = action(CreateUserAction::class, 'John', 'john@example.com');
Плюсы:
use LeMaX10\SimpleActions\UseCase;
class RegisterUserUseCase extends UseCase
{
protected function handle(array $data): \App\Models\User
{
$user = CreateUserAction::make()->run($data['name'], $data['email']);
SendWelcomeEmailAction::make()->run($user);
CreateUserProfileAction::make()->run($user, $data['profile']);
return $user;
}
}
// В контроллере
$user = RegisterUserUseCase::make()->run($request->validated());
// или
$user = usecase(RegisterUserUseCase::class, $request->validated());
Особенности:
CreateOrderAction::make()->withTransaction()->run($user, $items);
SomeReadOnlyUseCase::make()->withoutTransaction()->run($id);
Декларативно, без ручного Cache::remember(...) в каждом экшене.
$result = GetHeavyDataAction::make()
->remember('heavy:key', 60) // сек
->run($params);
$result = GetHeavyDataAction::make()
->rememberForever('heavy:key')
->run($params);
// Сгенерирует ключ автоматический по аргументам вызова с указанным префиксом
$result = GetHeavyDataAction::make()
->rememberAuto('heavyPrefix', 60)
->run($params);
Исключает дублирующиеся запросы в одном HTTP-запросе (например, при использовании одного экшена из разных слоёв). Я обычно замечал, что достаточно большая часть разработчиков при разработке проектов на разных слоях прибегает дублированию запросов, через повторые вызовы тех же методов сервисного слоя. В итоге у часто происходит проблема N+1.
// Первый вызов — выполнит handle() и запомнит результат
$user = GetUserAction::make()->memo()->run($userId);
// Повторный вызов с теми же аргументами — вернёт из памяти
$user = GetUserAction::make()->memo()->run($userId);
// Принудительно обновить мемоизированный результат
$user = GetUserAction::make()->memo(force: true)->run($userId);
// Запустить события даже при возврате из памяти
$user = GetUserAction::make()->memo(forceEvents: true)->run($userId);
По умолчанию для мемоизированных результатов события не запускаются При необходимости вы можете активировать аргумент forceEvents, чтобы события запустились.
В какой-то момент мне стало не хватать жизненого цикла экшенов и юзкейсов. Какое-то время я расставлял события в ручную, где-то прибегал к событиям моделей, после пришла мысль реализации Жизненного цикла экшенов и юзкейсов, за пример был взят подход из Eloquent ORM, в результате появились события: beforeRun, running, ran, failed, afterRun.
CreateUserAction::ran(function ($event) {
\Log::info('User created', ['id' => $event->result->id]);
});
// Мемоизация без повторных событий
CreateUserAction::make()->memo()->run($data);
// С принудительными событиями
CreateUserAction::make()->memo(forceEvents: true)->run($data);
// Обсерер
CreateUserAction::observe(UserNotification::class);
Возможности:
false).Подмена реализаций без изменения UseCase. Удобно в тестах и для разных окружений.
// Абстракция
abstract class NotificationAction extends Action {}
// Реализации
class SendEmailNotificationAction extends NotificationAction { /* ... */ }
class FakeNotificationAction extends NotificationAction { /* ... */ }
// В UseCase
app(NotificationAction::class)->run($user, 'Welcome!');
// В тестах
$this->app->bind(NotificationAction::class, FakeNotificationAction::class);
Структура:
app/
Actions/
User/
CreateUserAction.php
GetUserAction.php
UseCases/
User/
RegisterUserUseCase.php
Нейминг:
CreateUserAction, GetUserAction, SendInvoiceActionRegisterUserUseCase, CheckoutOrderUseCaseActions и UseCases дают:
Backend разработчик

Уже практически середина ноября, и мои парсеры давно собрали свежие данные. Сегодня я поделюсь анализом обновления рейтинга и новостями проекта.
В этом месяце мы добавили несколько технологий:
Fortran, ClickHouse, RabbitMQ, Kafka
Qt был перенесен из библиотеки в фреймворки
Удалён Ant Design из рейтинга
Главное улучшение этого месяца – полная переработка парсера. Мы значительно сократили процент нерелевантных вакансий, которые попадают в анализ. Алгоритм фильтрации стал умнее:
В ближайшем месяце ждёте ещё большие улучшения парсера.
В разделе “Часто задаваемые вопросы” мы подробно описали методологию расчёта рейтинга:
Go – поднялся на 3 пункта вверх, я услышал ваш фидбек начет go и после обновления парсера заменил поиск с golang на go.
JavaScript опередил C – JS занял 4-е место, сместив C на 5-е.
Топ-10 языков:
Jackson поднялась на 16 пунктов! caret обвалилась на 24 пункта!
Парсер стал точнее — результаты этого месяца более надёжны благодаря улучшениям
Спасибо, что следите за TrueIndex! Ваша обратная связь помогает нам становиться лучше.
Если у вас есть идеи по улучшению рейтинга или вы заметили неточности, пишите мне в Telegram
{message}