Подписывайтесь на наш Telegram-канал и будьте в курсе всех событий

Контекстное логирование с встроенными уведомлениями для Laravel

При разработке проектов я придерживаюсь принципа подробного логирования критически важных частей приложения. Это позволяет по логам понять, что происходило, что пошло не так и почему.

Однако при активном использовании логирования в проектах на Laravel я регулярно сталкивался с рядом неудобств:

  • Когда требуется залогировать важное событие и одновременно отправить уведомление о нём, приходится писать несколько строк кода: сформировать сообщение, записать его в лог, отправить уведомление. Это громоздко и утомляет.
  • Когда нужно отследить последовательность событий по логам, сузив контекст до конкретного пользователя, джобы, консольной команды или процесса, приходится заранее думать о структуре сообщений и постоянно добавлять в них дополнительные детали, чтобы потом найти их в логах. В итоге растёт когнитивная нагрузка, сообщения становятся слишком длинными.
  • При возникновении непредвиденных исключений необходимо отправлять уведомления в разные каналы и передавать дополнительный контекст: для какого пользователя произошла ошибка, в каком процессе, на каком сервере и т.д.

Чтобы решить эти (и не только) проблемы, я сначала добавил необходимые улучшения в одном из проектов, затем начал переносить их в другие, а в итоге оформил всё в отдельный пакет — faustoff/laravel-contextify. Им я и хочу с вами поделиться.

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 двумя основными (но не единственными) функциями:

  1. Встроенные уведомленияотправка уведомлений вместе с логированием в одну строку без необходимости разделения кода на несколько строк для логирования и отправки уведомления.
  2. Автоматическое добавление контекста — логи и уведомления автоматически включают дополнительные контекстные данные из настроенных провайдеров контекста (встроенные: ID трассировки, ID процесса, Имя хоста, Файл и строка вызова и другие), помогая сохранять сообщения короткими и чистыми, перемещая дополнительный контекст из самого сообщения в отдельную область.

Предоставляет фасад Contextify, совместимый с фасадом Log от Laravel: те же методы (debug, info, notice, warning, error, critical, alert, emergency) с идентичными параметрами, плюс цепочный метод notify().

Происхождение названия: “Contextify” объединяет Context (контекст) и Notify (уведомлять), отражая двойное назначение — обогащать логи контекстными данными и отправлять уведомления о событиях логирования.

Возможности

  • 📧 Поддержка уведомлений: Отправка уведомлений через почту, telegram или любой канал уведомлений Laravel
  • 🔍 Автоматическое добавление контекста: Логи и уведомления автоматически включают дополнительные контекстные данные из настроенных провайдеров контекста
  • 🔌 Пользовательские провайдеры контекста: Расширяйте встроенные провайдеры своими собственными
  • 🔄 Статические и динамические провайдеры**: Статические (кэшируемые) и динамические (обновляемые при каждом вызове) провайдеры
  • 🎯 Контекст на основе групп: Отдельные провайдеры контекста для логов и уведомлений
  • 📊 Уровни логирования PSR-3: Все стандартные уровни логирования (debug, info, notice, warning, error, critical, alert, emergency)
  • 🎨 Пользовательские уведомления: Расширяйте классы уведомлений и добавляйте пользовательские каналы
  • 🔔 Фильтрация каналов: Фильтрация каналов с параметрами only и except
  • 🔄 Удобный API: Цепочка методов для читаемого кода
  • Интеграция с Monolog: Интегрируется с логированием Laravel через процессоры Monolog

Требования

  • PHP 8.0 или выше
  • Laravel 8.0 или выше
  • Monolog 2.0 или выше

Установка

Установите пакет через 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.

Встроенные:

  • ProcessIdContextProvider: Добавляет ID текущего процесса PHP (pid)
  • TraceIdContextProvider: Генерирует уникальный 16-символьный шестнадцатеричный ID трассировки (trace_id) для распределённой трассировки
  • HostnameContextProvider: Добавляет имя хоста сервера (hostname)
  • EnvironmentContextProvider: Добавляет окружение приложения (environment)

Обновление статического контекста

Статический контекст кэшируется при загрузке приложения. Используйте touch() для ручного обновления, что полезно при форке процесса (например, для воркеров очередей) для генерации нового ID трассировки:

<?php

use Faustoff\Contextify\Facades\Contextify;
use Faustoff\Contextify\Context\Providers\TraceIdContextProvider;

// Обновить конкретный провайдер (например, сгенерировать новый ID трассировки)
Contextify::touch(TraceIdContextProvider::class);

// Обновить все статические провайдеры
Contextify::touch();

Динамические провайдеры контекста

Динамические провайдеры обновляют данные при каждом вызове логирования. Они реализуют DynamicContextProviderInterface.

Встроенные:

  • CallContextProvider: Добавляет путь к файлу и номер строки (file) и имя класса (class) вызывающего кода
  • PeakMemoryUsageContextProvider: Добавляет пиковое использование памяти в байтах (peak_memory_usage)
  • DateTimeContextProvider: Добавляет текущую дату и время в формате логов Laravel (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, необходимо:

  1. Создать пользовательский класс уведомлений с реализованным методом 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);
    }
}
  1. Создать пользовательский класс notifiable с реализованным методом 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');
    }
}
  1. Настроить Slack в config/services.php.

  2. Обновить 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"}

GitHub

Actions и UseCases в Laravel: практичный подход к бизнес-логике?

Сервисный слой часто разрастается до «толстых» классов, где сложно поддерживать код и переиспользовать отдельные части. В каком-то году по воле случая я смотрел ютубчик и наткнулся на подход Laravel Actions, основная суть заключается в создании простых классов, каждый из которых выполняет одну конкретную задачу (одно действие). Подход мне в целом понравился. Однако со временем, основной пакет стал обрастать фичами контекста, экшены как контроллер, как листенер, как консольная команда и т п, экшены стали размывать свою ответсвенность и хоть это и опционально, но я стал замечать, что во многих проектах это уже стало своего рода стандартом когда в один объект напихивают ответсвнности за все слои приложения.

Мне пришла идея создать пакет простых действий с решеним рутинных операций таких как “транзакции”, “кеширование”, “мемонизация”, “События”, “DIP”. А так же внедрить сценарный подход, когда есть объекты которые агрегируют простые и существующие действия в некий сценарий UseCase. Так пришло начало Simple Actions – атомарные Actions и сценарные UseCases. Пакет: lemax10/simple-actions (GitHub).

Ключевая идея

  • Action: один класс = одно действие.
  • UseCase: агрегирует несколько Actions в единый сценарий.
  • Плюс: транзакции, кеширование, мемоизация, события, DIP через контейнер.

Action: один объект = одно действие

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');

Плюсы:

  • Прозрачная ответственность и предсказуемость.
  • Лёгкая подмена реализации.
  • Удобное тестирование.

UseCase: сценарий из нескольких Actions

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());

Особенности:

  • UseCase выполняется в транзакции (идеально для «создать -> уведомить -> дополнить»).
  • Явная оркестрация шагов, легко читать и расширять.

Транзакции без бойлерплейта

  • UseCase — в транзакции по умолчанию.
  • Можно управлять из кода:
CreateOrderAction::make()->withTransaction()->run($user, $items);
SomeReadOnlyUseCase::make()->withoutTransaction()->run($id);

Кеширование результата (используя Laravel CacheManager)

Декларативно, без ручного 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).
  • Observer-подход как в Eloquent для группировки логики в отдельных объектах.

DIP через контейнер

Подмена реализаций без изменения 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

Нейминг:

  • Actions: CreateUserAction, GetUserAction, SendInvoiceAction
  • UseCases: RegisterUserUseCase, CheckoutOrderUseCase

Вместо итога

Actions и UseCases дают:

  • чистую, предсказуемую архитектуру;
  • высокую тестируемость и подменяемость (DIP);
  • меньше дублирования и бойлерплейта;
  • простые транзакции, декларативное кеширование и мемоизацию;
  • контроль жизненного цикла через события.
5

Ноябрь 2025: обновленный рейтинг технологий от TrueIndex

True index

Уже практически середина ноября, и мои парсеры давно собрали свежие данные. Сегодня я поделюсь анализом обновления рейтинга и новостями проекта.

Новости проекта

Новые технологии

В этом месяце мы добавили несколько технологий:

Fortran, ClickHouse, RabbitMQ, Kafka

Перенос технологий

Qt был перенесен из библиотеки в фреймворки

Удаленные технологии

Удалён Ant Design из рейтинга

Улучшения парсера

Главное улучшение этого месяца – полная переработка парсера. Мы значительно сократили процент нерелевантных вакансий, которые попадают в анализ. Алгоритм фильтрации стал умнее:

  • Лучше распознаёт контекст упоминания технологии в описании вакансии
  • Снижает влияние случайных совпадений по названию
  • Повышает точность рейтинга

В ближайшем месяце ждёте ещё большие улучшения парсера.

Улучшение FAQ

В разделе “Часто задаваемые вопросы” мы подробно описали методологию расчёта рейтинга:

  • Как собираются данные (HeadHunter, Habr Career)
  • Как применяется статистическая нормализация (Z-score)
  • Как рассчитывается финальный балл (3–50 пунктов)

Анализ рейтинга за ноябрь

Языки программирования

Go – поднялся на 3 пункта вверх, я услышал ваш фидбек начет go и после обновления парсера заменил поиск с golang на go.

JavaScript опередил C – JS занял 4-е место, сместив C на 5-е.

Топ-10 языков:

  1. SQL
  2. Python
  3. 1C
  4. JavaScript ⬆️
  5. C ⬇️
  6. Java
  7. C++
  8. PHP
  9. TypeScript ⬆️
  10. C# ⬇️
  11. Go ⬆️⬆️⬆️

Библиотеки и пакеты

Jackson поднялась на 16 пунктов! caret обвалилась на 24 пункта!

Базы данных

  • PostgreSQL — лидер среди реляционных БД
  • MongoDB — актуальна для NoSQL задач
  • Redis — высокий спрос для кэширования и real-time приложений
  • ClickHouse (новое) — растущий спрос на аналитические БД

DevOps & Облако

  • Docker — остаётся стандартом контейнеризации
  • Kubernetes — активное развитие
  • AWS — лидер облачных платформ в России
  • RabbitMQ (пополнение) — стабильная альтернатива Kafka
  • Kafka (пополнение) — растущий спрос на потоковую обработку

🎯 Ключевые выводы

Парсер стал точнее — результаты этого месяца более надёжны благодаря улучшениям

📈 Что дальше?

  • Оптимизация парсера — дальнейшее улучшение фильтрации вакансий
  • Расширение метрик — планируется добавить дополнительные метрики для более полной картины

Спасибо, что следите за TrueIndex! Ваша обратная связь помогает нам становиться лучше.

Если у вас есть идеи по улучшению рейтинга или вы заметили неточности, пишите мне в Telegram