Если вы видите это, значит, я еще не придумал, что написать.
Сервисный слой часто разрастается до «толстых» классов, где сложно поддерживать код и переиспользовать отдельные части. В каком-то году по воле случая я смотрел ютубчик и наткнулся на подход 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 дают:
{message}