Saga Lara Flow: стійкі Workflows та компенсаційні транзакції в Laravel Queues

Перекладено ШІ 0 Laravel News 08 серпня, 2026

Saga Lara Flow дозволяє описувати тривалі бізнес-процеси як прості послідовні методи без використання заплутаних ланцюжків Laravel jobs. Пакет автоматизує відкат невдалих транзакцій та забезпечує надійне керування паралельними діями у вашому коді.

Saga Lara Flow — це пакет для Laravel від Андрія Карпишина, який дозволяє описувати тривалі бізнес-процеси як звичайні PHP-методи поверх Laravel queues. Списання коштів, бронювання товарів, оформлення доставки — усі кроки йдуть один за одним у методі handle() без необхідності налаштовувати job chaining чи складні state machines.

Принцип роботи простий: пакет фіксує кожен успішний крок у базі даних. Коли ворклоу відновлюється або підхоплюється воркером, двигун знову запускає handle(), але метод $this->action() перехоплює виклики. Якщо крок уже був виконаний, він миттєво повертає результат із бази, не запускаючи клас дії повторно. Коли черга доходить до ще не виконаного коду, робота триває у звичному режимі. Якщо ж на якомусь етапі стається помилка, система запускає логіку компенсації для всіх попередніх кроків у зворотному порядку.

Основні можливості пакета:

  • Ворклоу як звичайні методи: метод handle() викликає дії послідовно. Зупинка між кроками реалізована через контроль потоку на базі винятків, а не через ланцюжки черг.
  • Компенсації: за допомогою compensateWith() можна зареєструвати зворотну дію або closure для кожного кроку. Це дозволяє автоматично відкотити успішні етапи, якщо наступні завершилися помилкою.
  • Сигнали: метод $this->signal() призупиняє виконання, доки зовнішній код не передасть необхідні дані. Підтримується налаштування таймаутів.
  • Паралельні блоки: можливість запускати кілька дій одночасно та отримувати їхні результати у вигляді масиву.
  • Дочірні ворклоу: запуск вкладених процесів із гнучкою політикою завершення (що стається з «дитиною», коли завершується «батько»).
  • Фіксація побічних ефектів: недетерміновані значення (наприклад, UUID або мітки часу) можна обгорнути в sideEffect(), щоб при повторних запусках використовувати початковий результат.
  • Запити за тегами: до ворклоу можна додавати теги (key/value) при створенні або під час виконання, а потім шукати потрібні процеси за класом, тегом чи статусом.
  • Команди Artisan: інструменти для перегляду списку запусків, інспектування стану, передачі сигналів, скасування процесів та очищення старих записів.

# Ворклоу та дії

Ворклоу наслідує клас Workflow і викликає дії через $this->action(). Класи дій (Actions) резолвляться з контейнера, тому їхні залежності впроваджуються автоматично разом із переданими аргументами:

use DiscoveryUkraine\SagaLaraFlow\Workflow;
 
class ProvisionAccountWorkflow extends Workflow
{
    public function handle(string $email): array
    {
        $tenantId = $this->action(CreateTenant::class, $email)->run();
        $this->action(SendWelcomeEmail::class, $email)->run();
 
        return ['tenant' => $tenantId];
    }
}
use DiscoveryUkraine\SagaLaraFlow\Action;
 
class CreateTenant extends Action
{
    public function handle(TenantRepository $tenants, string $email): string
    {
        return $tenants->provision($email)->id;
    }
}

Дії мають власні налаштування черги. Властивість $tries відповідає за кількість спроб, $timeout обмежує час кожної спроби, а expiresAt() встановлює загальний дедлайн для кроку. Якщо спроби вичерпано, ворклоу отримує ActionFailedException, а якщо пройдено дедлайн — FlowExpiredException. Обидві помилки можна обробити всередині методу.

Запуск процесів відбувається через фасад SagaFlow. Метод run() додає ворклоу в чергу, а runSync() виконує всі кроки синхронно, що зручно для тестування:

use DiscoveryUkraine\SagaLaraFlow\Facades\SagaFlow;
 
$run = SagaFlow::create(ProvisionAccountWorkflow::class)
    ->withArguments('jane@example.com')
    ->runSync();
 
$this->assertTrue($run->isCompleted());
$this->assertEquals('tenant-123', $run->result()['tenant']);

Механізм повторного відтворення (replay) працює коректно лише тоді, коли кожен крок повертає те саме значення, що і при першому запуску. Будь-які змінні дані потрібно фіксувати через sideEffect():

$reference = $this->sideEffect('reference', fn () => (string) Str::uuid());

# Компенсація невдалих транзакцій

Важлива частина патерну Saga — відкат дій. Для кожного кроку можна вказати дію для скасування, і ці компенсації спрацюють у зворотному порядку, якщо наступний крок завершиться помилкою:

public function handle(string $orderId): void
{
    $this->action(ChargeCard::class, $orderId)
        ->compensateWith(RefundCard::class, $orderId)
        ->run();
 
    $this->action(ReserveStock::class, $orderId)
        ->compensateWith(ReleaseStock::class, $orderId)
        ->run();
 
    // Якщо цей крок впаде, спочатку виконається ReleaseStock, а потім — RefundCard.
    $this->action(ShipOrder::class, $orderId)->run();
}

Для простих випадків замість класу можна передати closure. Якщо групу кроків потрібно відкочувати як єдине ціле, використовуйте $this->saga(). Це дає додатковий контроль: onCompensationFailure() визначає, чи зупиняти відкат при помилці компенсації, а compensateInParallel() дозволяє запускати відкати одночасно.

use DiscoveryUkraine\SagaLaraFlow\Enums\CompensationFailurePolicy;
 
$this->saga()
    ->onCompensationFailure(CompensationFailurePolicy::Continue)
    ->compensateInParallel()
    ->step(ChargeCard::class, $orderId)->compensateWith(RefundCard::class, $orderId)
    ->step(ReserveStock::class, $orderId)->compensateWith(ReleaseStock::class, $orderId)
    ->run();

# Очікування зовнішніх подій

Сигнали використовуються, коли процес потребує підтвердження від людини або відповіді від стороннього сервісу. $this->signal() призупиняє виконання та звільняє воркера. Виклик wait() відновлює роботу після отримання даних, а timeoutAfter() дозволяє задати граничний час очікування:

use DiscoveryUkraine\SagaLaraFlow\Exceptions\AwaitSignalTimeoutException;
 
try {
    $decision = $this->signal('approval')
        ->timeoutAfter(now()->addDay())
        ->wait();
} catch (AwaitSignalTimeoutException $e) {
    $this->action(AutoReject::class)->run();
}

Відправити сигнал можна з будь-якої частини програми. Метод signalIfRunning() поверне false замість винятку, якщо процес уже завершено або скасовано:

SagaFlow::loadFlow($runId)->signal('approval', ['approved' => true]);

Теги дозволяють знайти потрібний процес без збереження його ID. Їх можна додати при створенні через withTags() або всередині ворклоу через $this->tag(). Потім можна фільтрувати за класом, тегом чи статусом:

SagaFlow::query()
    ->whereWorkflow(ProvisionCompanyWorkflow::class)
    ->whereTag('company', $companyId)
    ->signalable()
    ->handles()
    ->first()
    ?->signal('owner-synced');

# Паралелізм, опціональні кроки та вкладеність

Незалежні завдання можна об'єднати в паралельний блок. За замовчуванням політика failFast() скасовує весь блок при першій же помилці:

[$pricing, $inventory, $reviews] = $this->parallel()
    ->action(FetchPricing::class, $sku)
    ->action(FetchInventory::class, $sku)
    ->action(FetchReviews::class, $sku)
    ->run();

Якщо крок не є критичним для всього процесу, використовуйте optionalAction() або continueOnFailure() із дефолтним значенням:

$score = $this->optionalAction(FetchRiskScore::class, $orderId)
    ->fallbackValueOnFail(0)
    ->run();

Ворклоу можуть викликати дочірні процеси через $this->child(), де ChildClosePolicy визначає подальшу долю «дитини» після завершення «батька»:

use DiscoveryUkraine\SagaLaraFlow\Enums\ChildClosePolicy;
 
$result = $this->child(ConfigureSubdomainWorkflow::class, $domain)
    ->onParentClose(ChildClosePolicy::Terminate)
    ->run();

Оскільки ворклоу може тривати тижнями, пакет підтримує версіонування. Метод version('v2') прив'язує запуск до конкретної версії коду: активні процеси допрацьовують на старій логіці, а нові запускають оновлений клас. Для моніторингу дедлайнів передбачена команда saga-flow:monitor, яку варто додати в Laravel scheduler:

Schedule::command('saga-flow:monitor')->everyMinute();

# Встановлення

Пакет вимагає PHP 8.5 та Laravel 13 і розповсюджується за ліцензією MIT:

composer require discovery-ukraine/saga-lara-flow
php artisan migrate
php artisan vendor:publish --tag="saga-lara-flow-config"

У конфігурації можна налаштувати підключення до бази даних, черги, механізми блокувань та хуки для мультитенентності. Для multi-tenant застосунків можна задати capture та restore closures, щоб ворклоу відновлювався з правильним контекстом орендаря.

Пакет також містить Artisan-команди для розробки та керування:

  • make:workflow та make:action: створення заготовок класів.
  • saga-flow:list: перегляд активних, очікуючих та невдалих процесів.
  • saga-flow:signal: відправка сигналів безпосередньо з консолі.
  • saga-flow:prune: очищення записів про завершені або скасовані запуски.

Детальна документація, розділи про тестування та мультитенентність доступні на sagalaraflow.dev. Вихідний код проекту можна знайти в репозиторії saga-lara-flow на GitHub.

Популярні

Інше, що варто прочитати

13 Оновлено 26 червня, 2026

Удосконалюйте свої проєкти Laravel за допомогою справжнього штучного інтелекту для кодування з Laravel Boost!

Готові підняти свій робочий процес у Laravel на новий рівень? У цій статті я розгляну Laravel Boost, інноваційний AI-допомічник для програмування, який зробить вашу розробку швидшою та продуктивнішою

32 Оновлено 26 червня, 2026

Інтеграція Laravel Socialite з бібліотекою Google Client PHP

Ви хочете навчитися, як інтегрувати Google OAuth у вашому проекті Laravel, використовуючи Socialite? Дізнайтеся, як налаштувати доступ до сервісів Google, таких як Календар, у нашій сьогоднішній статті

14 Оновлено 25 червня, 2025

Отримання параметрів команди в Laravel Artisan

Laravel спрощує доступ до аргументів та опцій у ваших кастомних командах Artisan, дозволяючи легко отримувати та валідувати параметри. Дізнайтеся, як ці вбудовані допоміжні методи можуть покращити ваш процес розробки!