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.