Поддержите проект, сделав пожертвование

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);
  • меньше дублирования и бойлерплейта;
  • простые транзакции, декларативное кеширование и мемоизацию;
  • контроль жизненного цикла через события.
1

Ноябрь 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

Типобезопасный каст значений из env() в config

Вы когда-нибудь ловили TypeError, просто потому что .env вернул строку вместо числа? Или получали неожиданное поведение из-за опечатки в булевом флаге falose вместо false?

🚨 Проблема

В файле .env можно задать некорректные значения, которые затем попадают в конфиг. В результате приложение может неожиданно падать с ошибкой 500 или вести себя непредсказуемо.

Пакет sushi-market/smart-cast

🔗 github.com/sushi-market/smart-cast

Этот пакет решает проблему: мы можем добавить уровень типобезопасной валидации и каста прямо в конфиг файлах. Если значение в .env невалидно — приложение сразу упадёт понятной ошибкой, а не в случайный момент времени.

Сейчас у нас есть два основных каста: stringToInt и stringToFloat
Передавая параметры в функцию, мы можем строго проверять значения в момент каста:

sign – если передано, то ограничивает значение только положительным или отрицательным. Полезно, например, для настройки TTL, который не может быть отрицательным.

strictType – по умолчанию true
При true каст "123.45" в int вызовет ошибку. Если указать false, значение будет приведено, но лучше оставлять строгий режим.

acceptsZero – по умолчанию true
Если установить false, ноль вызовет ошибку. Удобно для параметров вроде port.

acceptNull – по умолчанию false Если true, то переданный null вернёт null вместо исключения – полезно для опциональных значений в конфиге.

Примеры использования

  1. Валидация числового значения:
    Интервал должен быть целым положительным числом больше 0.
'default_interval_length' => stringToInt(
    value: env('INTERVALS_DEFAULT_LENGTH', 15),
    sign: \DF\NumberSign::POSITIVE,
    acceptsZero: false,
),
  1. Валидация булевого значения:
    debug может быть только true или false. Это исключает опечатки вроде falose, которые интерпретатор воспримет как true.
'debug' => stringToBoolean(
    value: env('APP_DEBUG', false),
),

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