PHPackages                             lemax10/simple-actions - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. lemax10/simple-actions

ActivePackage[Utility &amp; Helpers](/categories/utility)

lemax10/simple-actions
======================

Реализация паттерна простых переиспользуемых действий

v2.3.0(2mo ago)252263GPL-2.0-onlyPHPPHP &gt;=8.2

Since Jan 10Pushed 2mo ago4 watchersCompare

[ Source](https://github.com/LeMaX10/simple-actions)[ Packagist](https://packagist.org/packages/lemax10/simple-actions)[ Docs](https://pyankov.pro)[ RSS](/packages/lemax10-simple-actions/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (9)Dependencies (8)Versions (14)Used By (0)

Simple Actions
==============

[](#simple-actions)

Language: [Русский](README.md) | [English](README.en.md)

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

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

Установка
---------

[](#установка)

```
composer require lemax10/simple-actions
```

Artisan команды генерации
-------------------------

[](#artisan-команды-генерации)

Пакет добавляет команды для быстрого создания заготовок:

```
php artisan make:action User/CreateUser
php artisan make:usecase User/RegisterUser
```

Что будет создано:

- `app/Actions/User/CreateUserAction.php`
- `app/UseCases/User/RegisterUserUseCase.php`

Можно указать полное имя с суффиксом, если необходимо:

```
php artisan make:action Actions/User/CreateUserAction
php artisan make:usecase UseCases/User/RegisterUserUseCase
```

Флаг `--force` перезапишет существующий файл.

Основные возможности
--------------------

[](#основные-возможности)

- Простой и понятный API для создания Actions
- **UseCase** паттерн - агрегирование Actions в сценарии
- **DIP (SOLID)** - загрузка через Service Container, подмена реализаций
- Полный цикл жизни событий (beforeRun, running, ran, failed, afterRun)
- Observer паттерн подобно Eloquent
- Управление транзакциями БД
- Продвинутое кеширование результатов (поддержка штатного Laravel Cache драйвера или любого поддерживаемого им)
- **Мемоизация** - кеширование в памяти на время запроса
- **Idempotency** - защита от повторного параллельного выполнения и от возможного эффекта "гонка".
- Условное выполнение
- Хелперы для удобного использования

Быстрый старт
-------------

[](#быстрый-старт)

### Создание Action

[](#создание-action)

```
use LeMaX10\SimpleActions\Action;

class CreateUserAction extends Action
{
    protected function handle(string $name, string $email): User
    {
        return User::create([
            'name' => $name,
            'email' => $email,
        ]);
    }
}
```

### Использование

[](#использование)

```
// Создание и выполнение
$user = CreateUserAction::make()->run('John Doe', 'john@example.com');

// Через хелпер
$user = action(CreateUserAction::class, 'John Doe', 'john@example.com');

// Условное выполнение
$user = CreateUserAction::make()
    ->runIf($condition, 'John', 'john@example.com');

$user = CreateUserAction::make()
    ->runUnless($condition, 'John', 'john@example.com');
```

### Idempotency (опционально)

[](#idempotency-опционально)

Для защиты от повторного параллельного выполнения одного и того же действия:

```
$order = CreateOrderAction::make()
    ->idempotent("order:create:{$requestId}", 300)
    ->run($payload);

// Вычисление ключа от аргументов
$order = CreateOrderAction::make()
    ->idempotent(fn (array $payload) => 'order:' . $payload['external_id'])
    ->run($payload);

// Автогенерация ключа аналогично rememberAuto/memo
$order = CreateOrderAction::make()
    ->idempotentAuto('order:create', 300)
    ->run($payload);
```

Если ключ уже использовался, вернется сохраненный результат без повторного выполнения `handle`. Если такой же ключ сейчас "в процессе", будет выброшено исключение `ActionIdempotencyInProgressException`.

По умолчанию используется cache-based репозиторий. Репозиторий можно переключить:

```
$result = SomeAction::make()
    ->idempotentRepository('cache')
    ->idempotentStore('redis')
    ->idempotent('my:key', 300)
    ->run($payload);
```

Можно регистрировать собственные idempotency-драйверы через `IdempotencyRepositoryManager::extend(...)`. К примеру если Вам небходимо хранить блокировки в БД или абстрактном хранилище.

### UseCase - Сценарии из Actions

[](#usecase---сценарии-из-actions)

UseCase это полноценный Action, агрегирующий другие Actions:

```
use LeMaX10\SimpleActions\UseCase;

class RegisterUserUseCase extends UseCase
{
    protected function handle(array $data): User
    {
        // Все действия выполняются в транзакции
        $user = CreateUserAction::make()->run($data['name'], $data['email']);
        return tap($user, function() use($user, $data) {
            SendWelcomeEmailAction::make()->run($user);
            CreateUserProfileAction::make()->run($user, $data['profile']);
        });
    }
}

// Использование как обычного Action
$user = RegisterUserUseCase::make()->run($data);

// Или через хелпер
$user = usecase(RegisterUserUseCase::class, $data);
```

### Типизация результата

[](#типизация-результата)

Чтобы приложение сохраняло строгую типизацию при работе с `run()` и хелперами `action()`/`usecase()`, укажите возвращаемый тип через PHPDoc-дженерик:

```
use App\Models\User;
use LeMaX10\SimpleActions\Action;

/**
 * @extends Action
 */
class FindUserAction extends Action
{
    protected function handle(int $id): User
    {
        return User::query()->findOrFail($id);
    }
}

$user = FindUserAction::make()->run(1);            // User|false
$user = action(FindUserAction::class, 1);          // User|false
$maybe = FindUserAction::make()->runIf(false, 1);  // User|false|null

use App\Models\User;
use LeMaX10\SimpleActions\UseCase;

/**
 * @extends UseCase
 */
class RegisterUserUseCase extends UseCase
{
    protected function handle(array $data): User
    {
        return CreateUserAction::make()->run($data['name'], $data['email']);
    }
}

$user = RegisterUserUseCase::make()->run($data);   // User|false
$user = usecase(RegisterUserUseCase::class, $data); // User|false
```

`false` возможен, если выполнение было остановлено в `beforeRun`/`running` событии.

События
-------

[](#события)

### Цикл жизни событий

[](#цикл-жизни-событий)

Action имеет полный цикл жизни с 5 событиями:

1. **beforeRun** - перед началом выполнения
2. **running** - непосредственно перед вызовом handle()
3. **ran** - после успешного выполнения
4. **failed** - при ошибке выполнения
5. **afterRun** - всегда выполняется в конце

### Регистрация слушателей

[](#регистрация-слушателей)

```
// Простая регистрация
CreateUserAction::beforeRun(function (ActionBeforeRun $event) {
    Log::info('Creating user', $event->arguments);
});

CreateUserAction::ran(function (ActionRan $event) {
    Log::info('User created', ['user' => $event->result]);
});

CreateUserAction::failed(function (ActionFailed $event) {
    Log::error('Failed to create user', [
        'exception' => $event->exception
    ]);
});
```

### Остановка выполнения

[](#остановка-выполнения)

События `beforeRun` и `running` могут остановить выполнение, вернув `false`:

```
CreateUserAction::beforeRun(function (ActionBeforeRun $event) {
    if ($user->isBanned()) {
        return false; // Остановит выполнение
    }
});
```

### Observer паттерн

[](#observer-паттерн)

Создайте observer для группировки логики событий:

```
use LeMaX10\SimpleActions\Observers\ActionObserver;

class CreateUserActionObserver extends ActionObserver
{
    public function beforeRun(ActionBeforeRun $event): void
    {
        // Логика перед выполнением
    }

    public function ran(ActionRan $event): void
    {
        // Логика после успешного выполнения
    }

    public function failed(ActionFailed $event): void
    {
        // Логика при ошибке
    }
}

// Регистрация observer
CreateUserAction::observe(CreateUserActionObserver::class);
```

### Отключение событий

[](#отключение-событий)

```
CreateUserAction::withoutEvents(function () {
    CreateUserAction::make()->run('John', 'john@example.com');
});
```

### Локальные события для конкретного экземпляра

[](#локальные-события-для-конкретного-экземпляра)

Если нужно одноразово повесить хук/хуки только на один вызов:

```
CreateUserAction::make()
    ->before(fn () => Log::info('single before'))
    ->after(fn () => Log::info('single after'))
    ->run($data);

// Следующий вызов — уже без этих хуков
CreateUserAction::make()->run($data);
```

Доступные локальные методы: `before` (beforeRun), `runningLocal` (running), `then` (ran), `onFail` (failed), `after` (afterRun). \*\*Возврат `false` из локальных `before`/`runningLocal` останавливает только текущий экземпляр события.

### Условные локальные события

[](#условные-локальные-события)

Хелперы `*When` / `*Unless` запускают локальный колбэк по условию (Boolean или `Closure`, аргументы — такие же, что и у `run`):

```
GetUserAction::make()
    ->beforeWhen(fn ($id) => $id > 0, fn () => Log::debug('positive id'))
    ->afterUnless(false, fn () => Log::debug('always after'))
    ->run(10);
```

`Unless` инвертирует условие, `When`. Локальные хуки не влияют на другие экземпляры и не добавляют глобальных слушателей или событий.

Транзакции
----------

[](#транзакции)

### Автоматические транзакции

[](#автоматические-транзакции)

```
class CreateUserAction extends Action
{
    // Всегда выполнять в транзакции
    protected bool $singleTransaction = true;

    protected function handle(string $name, string $email): User
    {
        return User::create(['name' => $name, 'email' => $email]);
    }
}
```

### Динамическое управление

[](#динамическое-управление)

```
// Включить транзакцию
CreateUserAction::make()
    ->withTransaction()
    ->run('John', 'john@example.com');

// Отключить транзакцию (переопределяет $singleTransaction)
CreateUserAction::make()
    ->withoutTransaction()
    ->run('John', 'john@example.com');
```

Кеширование
-----------

[](#кеширование)

### Базовое кеширование

[](#базовое-кеширование)

```
// Кеширование на 60 секунд
$result = CalculateAction::make()
    ->remember('calc-key', 60)
    ->run($data);

// Постоянное кеширование
$result = CalculateAction::make()
    ->rememberForever('calc-key')
    ->run($data);
```

### Автогенерация ключей

[](#автогенерация-ключей)

```
// Ключ генерируется автоматически на основе аргументов
$result = CalculateAction::make()
    ->rememberAuto('prefix', 60)
    ->run($value);
```

### Теги кеша

[](#теги-кеша)

```
$result = GetUserDataAction::make()
    ->tags(['users', 'user-' . $userId])
    ->remember('user-data-' . $userId, 60)
    ->run($userId);

// Очистка по тегам
Cache::tags(['users'])->flush();
```

### Выбор драйвера кеша

[](#выбор-драйвера-кеша)

```
$result = HeavyCalculationAction::make()
    ->store('redis')
    ->remember('calculation-key', 3600)
    ->run($data);
```

### Условное кеширование

[](#условное-кеширование)

```
// Кешировать только если условие истинно
$result = GetDataAction::make()
    ->remember('data-key', 60)
    ->cacheWhen($user->isPremium())
    ->run();

// С closure
$result = CalculateAction::make()
    ->remember('calc-key', 60)
    ->cacheWhen(fn ($value) => $value > 100)
    ->run($value);

// Кешировать если НЕ выполнено условие
$result = GetDataAction::make()
    ->remember('data-key', 60)
    ->cacheUnless($user->isAdmin())
    ->run();
```

### Управление кешем

[](#управление-кешем)

```
$action = GetDataAction::make()->remember('key', 60);

// Проверка наличия в кеше
if ($action->isCached('key')) {
    // ...
}

// Получение ключа кеша
$cacheKey = $action->getCacheKey();

// Удаление из кеша
$action->forget('key');
```

Мемоизация в памяти
-------------------

[](#мемоизация-в-памяти)

Мемоизация позволяет сохранять результаты выполнения Action в памяти PHP на время текущего запроса. Это помогает избежать повторного выполнения одинаковых действий, запросов к БД или внешним API в рамках одного запроса.

### Отличие от кеширования

[](#отличие-от-кеширования)

- **Кеширование** (`remember()`) - сохраняет результат в кеш (CacheManager Laravel) между запросами
- **Мемоизация** (`memo()`) - сохраняет результат в памяти PHP только на время текущего запроса

### Базовое использование

[](#базовое-использование)

```
// Первый вызов - выполнит handle()
$user = GetUserAction::make()->memo()->run($userId);

// Повторный вызов с теми же аргументами - вернёт результат из памяти
$user = GetUserAction::make()->memo()->run($userId); // handle() не выполнится

// Другие аргументы - выполнит handle() снова
$otherUser = GetUserAction::make()->memo()->run($otherUserId);
```

### Принудительное обновление

[](#принудительное-обновление)

```
// Сохраняем результат
$data = FetchDataAction::make()->memo()->run($params);

// Получаем мемоизированный результат
$data = FetchDataAction::make()->memo()->run($params);

// Принудительно обновляем результат
$freshData = FetchDataAction::make()
    ->memo(force: true)
    ->run($params); // handle() выполнится заново

// Теперь memo() возвращает обновлённый результат
$data = FetchDataAction::make()->memo()->run($params); // Вернёт $freshData
```

### События и мемоизация

[](#события-и-мемоизация)

По умолчанию, когда результат берётся из памяти (мемоизирован), события **НЕ запускаются** повторно. Это связано с производительностью и рализовано в качестве оптимизации.

```
CreateUserAction::ran(function($event) {
    Log::info('User created'); // Логируем только при реальном создании
});

// Первый вызов - handle() выполнится, события запустятся
$user1 = CreateUserAction::make()->memo()->run($data);
// Лог: "User created"

// Второй вызов - результат из памяти, события НЕ запустятся
$user2 = CreateUserAction::make()->memo()->run($data);
// Лог: (пусто)
```

Если все-таки по какой-то причине необходимо запускать события даже для мемоизированных результатов, используйте аргумент `forceEvents`:

```
CreateUserAction::ran(function($event) {
    Cache::tags(['users'])->flush(); // Нужно каждый раз
});

// События запустятся даже для мемоизированного результата
$user = CreateUserAction::make()
    ->memo(forceEvents: true)
    ->run($data);
```

Небольшие рекомендации:

**Когда использовать forceEvents:**

- Нужны побочные эффекты в событиях (очистка кеша, уведомления)
- События используются для аудита/логирования каждого обращения
- Отладка - хотите видеть все вызовы

**Когда НЕ использовать forceEvents:**

- События только для внутренней логики Action
- Производительность критична (overhead дублирование эвентов будет замедлять работу приложения)
- События дублируют то, что уже произошло при первом вызове

### Пользовательский ключ мемоизации

[](#пользовательский-ключ-мемоизации)

По умолчанию ключ генерируется на основе аргументов. Однако можно задать свой ключ:

```
// Использовать пользовательский ключ вместо хеша аргументов
$result = CalculateAction::make()
    ->memo(key: 'my-custom-key')
    ->run($value1, $value2);

// Вернёт тот же результат, даже с другими аргументами!
$result = CalculateAction::make()
    ->memo(key: 'my-custom-key')
    ->run($differentValue1, $differentValue2);
```

### Управление мемоизацией (для сложных сценариев)

[](#управление-мемоизацией-для-сложных-сценариев)

```
$action = GetDataAction::make();

// Проверить, мемоизирован ли результат
if ($action->isMemoized([$userId])) {
    // Результат уже в памяти
}

// Забыть конкретный результат
$action->memoForget([$userId]);

// Забыть все результаты данного Action
GetDataAction::memoFlush();

// Очистить мемоизацию всех Actions (редко используется)
Action::memoFlushAll();

// Получить количество мемоизированных результатов
$count = GetDataAction::getMemoizedCount();
```

### Практические примеры

[](#практические-примеры)

**Избежать N+1 проблемы в UseCase:**

```
class GetUserPermissionsAction extends Action
{
    protected function handle(int $userId): array
    {
        // Тяжёлый запрос к БД
        return DB::table('permissions')
            ->join('user_permissions', ...)
            ->where('user_id', $userId)
            ->get()
            ->toArray();
    }
}

class ProcessUsersUseCase extends UseCase
{
    protected function handle(array $userIds): array
    {
        $results = [];

        foreach ($userIds as $userId) {
            // Если userId повторяется - запрос не выполнится повторно
            $permissions = GetUserPermissionsAction::make()
                ->memo()
                ->run($userId);

            $results[] = ProcessUserAction::make()->run($userId, $permissions);
        }

        return $results;
    }
}
```

**Использование между слоями приложения:**

```
// Controller
class OrderController
{
    public function show(int $orderId)
    {
        // Первый запрос
        $order = GetOrderAction::make()->memo()->run($orderId);

        // Вызывается UseCase, который тоже запрашивает заказ
        $invoice = GenerateInvoiceUseCase::make()->run($orderId);

        return view('order.show', compact('order', 'invoice'));
    }
}

// UseCase
class GenerateInvoiceUseCase extends UseCase
{
    protected function handle(int $orderId): Invoice
    {
        // Не выполнит запрос повторно - возьмёт из памяти
        $order = GetOrderAction::make()->memo()->run($orderId);

        return GeneratePdfInvoice::make()->run($order);
    }
}
```

**Кеширование внешних API запросов:**

```
class FetchExchangeRateAction extends Action
{
    protected function handle(string $currency): float
    {
        // Запрос к внешнему API
        return Http::get("https://api.example.com/rate/{$currency}")
            ->json('rate');
    }
}

// В любом месте приложения
$rate1 = FetchExchangeRateAction::make()->memo()->run('USD');
$rate2 = FetchExchangeRateAction::make()->memo()->run('USD'); // Не запросит API
$rate3 = FetchExchangeRateAction::make()->memo()->run('EUR'); // Запросит для EUR
```

**Комбинирование с кешированием:**

```
// Мемоизация + кеширование = максимальная производительность
$report = GenerateReportAction::make()
    ->memo() // В памяти на время запроса
    ->remember('report-key', 3600) // В Redis на час
    ->run($params);

// Первый запрос: выполнит handle() -> сохранит в Redis и в память
// Второй запрос в том же HTTP запросе: возьмёт из памяти
// Третий запрос в новом HTTP запросе: возьмёт из Redis
```

### Когда использовать мемоизацию

[](#когда-использовать-мемоизацию)

✅ **Используйте memo() когда:**

- Action вызывается несколько раз в одном запросе с одинаковыми аргументами
- Нужно избежать дублирования запросов к БД/API/дублирования выполнения сложной логики или вычислений
- Action используется в циклах или рекурсивно
- Action вызывается из разных слоёв (Controller -&gt; UseCase -&gt; другие Actions)
- Результат нужен только на время текущего запроса

❌ **Не используйте memo() когда:**

- Action вызывается только один раз за запрос
- Результат должен быть свежим при каждом вызове
- Action имеет побочные эффекты (отправка email, запись в БД)
- Нужно сохранить результат между разными HTTP запросами (используйте `remember()`)

### Производительность

[](#производительность)

Мемоизация добавляет минимальный overhead, однако облегчает рутинные действия и избавляет от дублирования запросов к БД/Кешу. Расценивайте это как своего рода цену за удобство. Пример:

- Без `memo()`: ~0 overhead
- С `memo()` (первый вызов): ~5-10μs (генерация хеша)
- С `memo()` (повторные вызовы): ~1-2μs (проверка массива)
- С `memo(forceEvents: true)`: +50-100μs (запуск событий)
-

**Оптимизация событий:**По умолчанию для мемоизированных результатов события не запускаются, что экономит приблизитльно ~50-100μs на каждый повторный вызов, в зависимости от логики которую вы заложили. Используйте `forceEvents: true` только когда действительно необходимо вызывать события повторно при повторном вызове экшена.

Комплексное использование
-------------------------

[](#комплексное-использование)

Все возможности можно комбинировать:

```
class CreateOrderAction extends Action
{
    protected bool $singleTransaction = true;

    protected function handle(User $user, array $items): Order
    {
        $order = Order::create(['user_id' => $user->id]);

        foreach ($items as $item) {
            $order->items()->create($item);
        }

        return $order;
    }
}

// Регистрация observer
CreateOrderAction::observe(OrderActionObserver::class);

// Использование с транзакцией, кешированием и событиями
$order = CreateOrderAction::make()
    ->withTransaction()
    ->rememberAuto('order', 3600)
    ->cacheWhen(fn ($user) => $user->isPremium())
    ->tags(['orders', 'user-' . $user->id])
    ->store('redis')
    ->run($user, $items);
```

Лучшие практики
---------------

[](#лучшие-практики)

### 1. Один Action - одно действие, UseCase - сценарий

[](#1-один-action---одно-действие-usecase---сценарий)

```
// ✅ Хорошо - атомарные действия
class CreateUserAction extends Action { }
class SendEmailAction extends Action { }
class LogActivityAction extends Action { }

// ✅ Хорошо - UseCase агрегирует действия
class RegisterUserUseCase extends UseCase {
    protected function handle($data) {
        $user = CreateUserAction::make()->run($data);
        SendEmailAction::make()->run($user);
        LogActivityAction::make()->run($user);
        return $user;
    }
}

// ❌ Плохо - слишком общее
class UserAction extends Action { }
```

### 2. Используйте абстракции для взаимозаменяемых Actions (DIP)

[](#2-используйте-абстракции-для-взаимозаменяемых-actions-dip)

```
// ✅ Хорошо - зависимость от абстрактного класса
abstract class NotificationAction extends Action {
    // handle() реализуют дочерние классы
}

class SendEmailAction extends NotificationAction {
    protected function handle(User $user, string $message): bool { /* ... */ }
}

class SendSmsAction extends NotificationAction {
    protected function handle(User $user, string $message): bool { /* ... */ }
}

class NotifyUserUseCase extends UseCase {
    protected function handle(User $user, string $message) {
        // Зависимость от абстракции - легко подменить через контейнер
        return app(NotificationAction::class)->run($user, $message);
    }
}

// ❌ Плохо - невозможность подмены в тестах
class NotifyUserUseCase extends UseCase {
    protected function handle(User $user, string $message) {
        // Жестко привязан к SendEmailAction, нельзя подменить
        return (new SendEmailAction())->run($user, $message);
    }
}
```

### 3. Используйте типизацию

[](#3-используйте-типизацию)

```
class CreateUserAction extends Action
{
    protected function handle(
        string $name,
        string $email,
        ?string $phone = null
    ): User {
        // ...
    }
}
```

### 3. UseCase для агрегирования Actions

[](#3-usecase-для-агрегирования-actions)

UseCase - это тот же Action, но предназначенный для координации множества других Actions в единый сценарий:

```
use LeMaX10\SimpleActions\UseCase;

class RegisterUserUseCase extends UseCase
{
    // UseCase поддерживает все возможности Action:
    // - События (beforeRun, running, ran, failed, afterRun)
    // - Транзакции (по умолчанию включены)
    // - Кеширование

    protected function handle(array $data): User
    {
        // UseCase координирует выполнение нескольких Actions
        $user = CreateUserAction::make()->run($data['name'], $data['email']);

        SendWelcomeEmailAction::make()->run($user);

        CreateUserProfileAction::make()->run($user, $data['profile']);

        NotifyAdminAction::make()->run($user);

        return $user;
    }
}

// Использование UseCase как обычного Action
$user = RegisterUserUseCase::make()
    ->remember('user-registration-' . $email, 300) // Можно кешировать
    ->run($data);

// UseCase поддерживает события
RegisterUserUseCase::ran(function ($event) {
    Log::info('User registered', ['user' => $event->result]);
});

// UseCase выполняется в транзакции (по умолчанию)
// Если любое вложенное действие упадет - откатится всё
```

**Преимущества UseCase:**

- Все Actions в UseCase выполняются в единой транзакции
- UseCase можно кешировать целиком
- События отслеживают весь сценарий
- Переиспользуемая бизнес-логика

Dependency Inversion Principle (SOLID)
--------------------------------------

[](#dependency-inversion-principle-solid)

Actions загружаются через Laravel Service Container (`app(static::class)`), что позволяет применять принцип инверсии зависимостей:

### Подход 1: Подмена через абстрактные классы

[](#подход-1-подмена-через-абстрактные-классы)

```
// Базовый абстрактный класс (вместо интерфейса)
abstract class SendNotificationAction extends Action {

}

// Реальная реализация
class SendEmailNotificationAction extends SendNotificationAction {
    protected function handle(User $user, string $message): bool {
        Mail::to($user)->send(new Notification($message));
        return true;
    }
}

// Тестовая реализация
class FakeSendNotificationAction extends SendNotificationAction {
    protected function handle(User $user, string $message): bool {
        Log::info('Fake notification sent');
        return true;
    }
}

// Регистрация в ServiceProvider
public function register() {
    $this->app->bind(
        SendNotificationAction::class,
        SendEmailNotificationAction::class
    );
}

// UseCase зависит от абстракции, а не конкретного объекта
class RegisterUserUseCase extends UseCase {
    protected function handle(array $data): User {
        $user = CreateUserAction::make()->run($data);

        // Получаем реализацию из контейнера
        app(SendNotificationAction::class)->run($user, 'Welcome!');

        return $user;
    }
}

// В тестах можно подменить
public function test_registration() {
    $this->app->bind(
        SendNotificationAction::class,
        FakeSendNotificationAction::class  // Подмена!
    );

    $user = RegisterUserUseCase::make()->run($data);

    $this->assertDatabaseHas('users', ['email' => $data['email']]);
}
```

### Подход 2: Подмена конкретного класса

[](#подход-2-подмена-конкретного-класса)

```
// UseCase зависит от конкретного класса
class RegisterUserUseCase extends UseCase {
    protected function handle(array $data): User {
        $user = CreateUserAction::make()->run($data);

        // Использование конкретного класса
        SendEmailAction::make()->run($user, 'Welcome!');

        return $user;
    }
}

// В тестах подменяем конкретный класс на fake
public function test_registration() {
    // Подменяем SendEmailAction на FakeEmailAction
    $this->app->bind(SendEmailAction::class, FakeEmailAction::class);

    $user = RegisterUserUseCase::make()->run($data);

    $this->assertDatabaseHas('users', ['email' => $data['email']]);
}
```

### Подход 3: Регистрация по строковым ключам (менее предпочтительный, но возможный способ)

[](#подход-3-регистрация-по-строковым-ключам-менее-предпочтительный-но-возможный-способ)

```
// В ServiceProvider
public function register() {
    $this->app->bind('notification.action', function ($app) {
        if ($app->environment('testing')) {
            return new FakeNotificationAction();
        }
        return new SendEmailNotificationAction();
    });
}

// UseCase использует строковой ключ
class RegisterUserUseCase extends UseCase {
    protected function handle(array $data): User {
        $user = CreateUserAction::make()->run($data);

        app('notification.action')->run($user, 'Welcome!');

        return $user;
    }
}
```

### Внедрение зависимостей через конструктор

[](#внедрение-зависимостей-через-конструктор)

```
class SendEmailAction extends Action {
    public function __construct(
        protected Mailer $mailer,
        protected LoggerInterface $logger
    ) {
        parent::__construct();
    }

    protected function handle(User $user, string $message): void {
        $this->mailer->send($user->email, $message);
        $this->logger->info('Email sent', ['user' => $user->id]);
    }
}

// Laravel автоматически резолвит зависимости
$result = SendEmailAction::make()->run($user, 'Hello');
```

### Глобальная подмена для тестирования

[](#глобальная-подмена-для-тестирования)

```
// В тестах
class RegisterUserTest extends TestCase {
    protected function setUp(): void {
        parent::setUp();

        // Глобально подменяем тяжелые Actions на фейки
        $this->app->bind(SendEmailAction::class, FakeEmailAction::class);
        $this->app->bind(NotifySlackAction::class, FakeSlackAction::class);
    }

    public function test_user_registration() {
        // Все UseCase будут использовать фейковые Actions
        $user = RegisterUserUseCase::make()->run($data);

        $this->assertTrue($user->exists);
    }
}
```

### Условная подмена

[](#условная-подмена)

```
// В ServiceProvider
public function register() {
    // В зависимости от окружения используем разные реализации
    if ($this->app->environment('testing')) {
        $this->app->bind(PaymentActionInterface::class, FakePaymentAction::class);
    } elseif ($this->app->environment('local')) {
        $this->app->bind(PaymentActionInterface::class, SandboxPaymentAction::class);
    } else {
        $this->app->bind(PaymentActionInterface::class, StripePaymentAction::class);
    }
}
```

### Преимущества DIP в Actions

[](#преимущества-dip-в-actions)

- **Тестируемость**: легко подменять реализации в тестах
- **Гибкость**: можно менять реализацию без изменения UseCase
- **Изоляция**: UseCase зависят от абстракций, а не конкретных классов
- **Переиспользование**: разные реализации одной абстракции
- **Feature Flags**: включать/выключать функциональность через контейнер

### Почему не интерфейсы?

[](#почему-не-интерфейсы)

⚠**Важно**: Нельзя использовать интерфейсы с конкретной сигнатурой `run()`, так как базовый контракт `Action` уже определяет `run(...$args): mixed` с variadic параметрами. Любой другой интерфейс с конкретной сигнатурой будет несовместим.

**Решение**: Используйте абстрактные классы или подменяйте конкретные классы через контейнер.

Хелперы
-------

[](#хелперы)

Пакет предоставляет удобные хелперы:

### `action()` - Быстрое выполнение Action

[](#action---быстрое-выполнение-action)

```
// Быстрое выполнение Action
$result = action(CalculateAction::class, $data);

// Эквивалентно:
$result = CalculateAction::make()->run($data);
```

### `usecase()` - Быстрое выполнение UseCase

[](#usecase---быстрое-выполнение-usecase)

```
// Быстрое выполнение UseCase
$user = usecase(RegisterUserUseCase::class, $data);

// Эквивалентно:
$user = RegisterUserUseCase::make()->run($data);
```

### `action_with()` - Action с конфигурацией

[](#action_with---action-с-конфигурацией)

Позволяет сконфигурировать Action перед выполнением через callback:

```
// С мемоизацией
$user = action_with(
    GetUserAction::class,
    fn(Action $action) => $action->memo(),
    $userId
);

// С кешированием
$report = action_with(
    GenerateReportAction::class,
    fn(Action $action) => $action->rememberAuto('reports', 3600),
    $from, $to
);

// Комбинация опций
$result = action_with(
    ProcessOrderAction::class,
    fn(Action $action) => $action->memo()->withTransaction(),
    $orderId, $items
);

// С тегами кеша
$data = action_with(
    GetUserDataAction::class,
    fn(Action $action) => $action->remember('user-'.$id, 3600)->tags(['users']),
    $userId
);
```

### `usecase_with()` - UseCase с конфигурацией

[](#usecase_with---usecase-с-конфигурацией)

Позволяет сконфигурировать UseCase перед выполнением через callback:

```
// С мемоизацией
$user = usecase_with(
    RegisterUserUseCase::class,
    fn(UseCase $usecase) => $usecase->memo(),
    $userId
);

// С кешированием
$report = usecase_with(
    GenerateFinanceReportUseCase::class,
    fn(UseCase $usecase) => $usecase->rememberAuto('reports', 3600),
    $from, $to
);

// Комбинация опций
$result = usecase_with(
    GenerateFinanceReportUseCase::class,
    fn(UseCase $usecase) => $usecase->memo()->withTransaction(),
    $orderId, $items
);

// С тегами кеша
$data = usecase_with(
    GenerateFinanceReportFromUserUseCase::class,
    fn(UseCase $usecase) => $usecase->remember('user-'.$userModel->getKey(), 3600)->tags(['reports']),
    $userModel
);
```

### `generate_args_hash()` - Генерация хеша аргументов

[](#generate_args_hash---генерация-хеша-аргументов)

Функция для генерации MD5 хеша из массива аргументов. Используется внутри пакета для мемоизации и кеширования, но доступна и для внешнего использования:

```
// Генерирует хеш из аргументов
$hash = generate_args_hash([$userId, $type, ['option' => 'value']]);
// Результат: "5d41402abc4b2a76b9719d911017c592"

// Использование для создания уникальных ключей
$cacheKey = "custom-key:" . generate_args_hash($params);
Cache::remember($cacheKey, 3600, fn() => heavyCalculation($params));
```

**Почему она появилась?:**

- Использует `json_encode` для производительности (обычно быстрее чем `serialize`)
- Автоматический fallback на `serialize` для сложных объектов (Closure, Resources)
- Возвращает MD5 хеш для компактности ключей
- Чтобы не дублировать в нескольких местах (Кеширование, мемоизация) вынесена в отдельный хелпер

### Использование в контроллерах

[](#использование-в-контроллерах)

Хелперы особенно удобны в контроллерах и сервисах:

```
class UserController extends Controller
{
    public function register(Request $request)
    {
        $user = usecase(RegisterUserUseCase::class, $request->validated());

        return response()->json(['user' => $user]);
    }

    public function show(int $userId) // Вызываем через инъекцию
    {
        // С мемоизацией для избежания повторных запросов (Грубый пример для демонтрации)
        $user = action_with(
            GetUserAction::class,
            static fn(Action $action) => $action->memo()
            $userId
        );

        return view('user.show', compact('user'));
    }

    public function sendEmail(User $user)
    {
        action(SendEmailAction::class, $user, 'Welcome!');

        return back()->with('success', 'Email sent');
    }
}
```

### 4. Кешируйте тяжелые операции

[](#4-кешируйте-тяжелые-операции)

```
class GenerateReportAction extends Action
{
    protected function handle(Carbon $from, Carbon $to): Report
    {
        // Тяжелые вычисления
    }
}

$report = GenerateReportAction::make()
    ->rememberAuto('reports', 3600)
    ->tags(['reports'])
    ->run($from, $to);
```

Лицензия
--------

[](#лицензия)

GPL-2.0-only

Автор
-----

[](#автор)

Vladimir Pyankov (aka LeMaX10)

- Email:
- Website:

###  Health Score

50

—

FairBetter than 95% of packages

Maintenance86

Actively maintained with recent releases

Popularity25

Limited adoption so far

Community13

Small or concentrated contributor base

Maturity62

Established project with proven stability

 Bus Factor1

Top contributor holds 85.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~103 days

Recently: every ~39 days

Total

9

Last Release

67d ago

Major Versions

v1.0.2 → v2.0.02025-11-12

PHP version history (3 changes)v1.0.0PHP &gt;=7.4.0

v2.0.0PHP &gt;=8.0.0

v2.1.0PHP &gt;=8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/ef45edc1714e9db2e22281c81519aacedecf52235e89e3c5a2434a12b9b3f87c?d=identicon)[LeMaX10](/maintainers/LeMaX10)

---

Top Contributors

[![LeMaX10](https://avatars.githubusercontent.com/u/10564391?v=4)](https://github.com/LeMaX10 "LeMaX10 (6 commits)")[![slider23](https://avatars.githubusercontent.com/u/142277?v=4)](https://github.com/slider23 "slider23 (1 commits)")

---

Tags

phplaravelhelpersactions

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/lemax10-simple-actions/health.svg)

```
[![Health](https://phpackages.com/badges/lemax10-simple-actions/health.svg)](https://phpackages.com/packages/lemax10-simple-actions)
```

###  Alternatives

[markwalet/nova-modal-response

A Laravel Nova asset for Modal responses on an action.

17818.7k](/packages/markwalet-nova-modal-response)[ronasit/laravel-helpers

Provided helpers function and some helper class.

2082.5k25](/packages/ronasit-laravel-helpers)[tomshaw/electricgrid

A feature-rich Livewire package designed for projects that require dynamic, interactive data tables.

119.2k](/packages/tomshaw-electricgrid)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
