PHP code example of lemax10 / simple-actions

1. Go to this page and download the library: Download lemax10/simple-actions library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

lemax10 / simple-actions example snippets


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', '[email protected]');

// Через хелпер
$user = action(CreateUserAction::class, 'John Doe', '[email protected]');

// Условное выполнение
$user = CreateUserAction::make()
    ->runIf($condition, 'John', '[email protected]');

$user = CreateUserAction::make()
    ->runUnless($condition, 'John', '[email protected]');

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

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

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

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

/**
 * @extends Action<User>
 */
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<User>
 */
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

// Простая регистрация
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
    ]);
});

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

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', '[email protected]');
});

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

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

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

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', '[email protected]');

// Отключить транзакцию (переопределяет $singleTransaction)
CreateUserAction::make()
    ->withoutTransaction()
    ->run('John', '[email protected]');

// Кеширование на 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');

// Первый вызов - выполнит 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);
// Лог: (пусто)

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

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

// Использовать пользовательский ключ вместо хеша аргументов
$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();

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

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

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

// ✅ Хорошо - атомарные действия
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 { }

// ✅ Хорошо - зависимость от абстрактного класса
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);
    }
}

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

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 выполняется в транзакции (по умолчанию)
// Если любое вложенное действие упадет - откатится всё

// Базовый абстрактный класс (вместо интерфейса)
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']]);
}

// 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']]);
}

// В 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);
    }
}

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

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

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

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

// С мемоизацией
$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
);

// С мемоизацией
$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
);

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

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

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

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

$report = GenerateReportAction::make()
    ->rememberAuto('reports', 3600)
    ->tags(['reports'])
    ->run($from, $to);
bash
php artisan make:action User/CreateUser
php artisan make:usecase User/RegisterUser
bash
php artisan make:action Actions/User/CreateUserAction
php artisan make:usecase UseCases/User/RegisterUserUseCase