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

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

Вакансии

Партнёры и друзья

Помощь в разработке вашего проекта на Laravel

Независимо от сложности проекта эти кампании помогают сообществу и всем его участникам воплощать идеи в элегантные приложения.

Присоединиться

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

Перейти

Подкасты c зажигательными эпизодами, которые заставят задуматься и приведут к новым перспективам.

Перейти

Делятся опытом, находят друзей и обсуждают разработку и сопровождение любых бэкендов на PHP.

Перейти