Installation
composer require brzuchal/saga-symfony
Ensure your project uses Symfony 5.4+ (or Laravel with Symfony components).
Register the Bundle
Add to config/bundles.php (Symfony) or manually bootstrap in Laravel:
// config/app.php (Laravel)
'providers' => [
// ...
Brzuchal\SagaSymfony\SagaBundle::class,
],
First Use Case: Basic Saga Define a saga class (Symfony-style) and register it as a service:
// src/Saga/ExampleSaga.php
namespace App\Saga;
use Brzuchal\SagaSymfony\Saga\Saga;
class ExampleSaga extends Saga
{
public function doSomething(): void
{
// Business logic here
}
}
Trigger via command or event listener:
$saga = $container->get(ExampleSaga::class);
$saga->execute();
Event Listeners
Attach sagas to Symfony events (or Laravel events via EventDispatcher):
// src/EventListener/SagaListener.php
use Brzuchal\SagaSymfony\Saga\SagaInterface;
class SagaListener
{
public function __construct(private SagaInterface $saga) {}
public function onOrderCreated(): void
{
$this->saga->execute();
}
}
Command-Based Sagas
Use Symfony’s Command component to manually trigger sagas:
// src/Command/RunSagaCommand.php
namespace App\Command;
use Brzuchal\SagaSymfony\Saga\SagaInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class RunSagaCommand extends Command
{
protected static $defaultName = 'app:run-saga';
public function __construct(private SagaInterface $saga) {}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->saga->execute();
return Command::SUCCESS;
}
}
Dependency Injection Inject services (e.g., repositories, clients) into sagas:
// src/Saga/PaymentSaga.php
class PaymentSaga extends Saga
{
public function __construct(
private PaymentRepository $repository,
private PaymentGateway $gateway
) {}
public function processPayment(): void
{
$this->repository->find(...);
$this->gateway->charge(...);
}
}
Symfony\Bridge\Laravel\Container to bridge services.DB::transaction() for atomicity.No Built-in Persistence The package lacks a default storage layer. Implement your own (e.g., Doctrine, Laravel Eloquent) to track saga state:
// Example: Store saga state in DB
class SagaRepository {
public function save(Saga $saga): void {
// Custom logic
}
}
Symfony-Centric Design
Assumes Symfony’s EventDispatcher and Container. In Laravel, wrap dependencies manually or use a bridge like symfony/bridge-laravel.
Error Handling
Sagas throw exceptions by default. Implement retry logic (e.g., with symfony/messenger) or compensating transactions.
public function debugState(): array {
return ['step' => $this->currentStep, 'data' => $this->data];
}
monolog to log saga execution:
$this->logger->info('Saga executed', ['saga' => get_class($this)]);
Custom Steps
Extend Saga to add lifecycle hooks:
abstract class CustomSaga extends Saga {
protected function beforeExecute(): void { /* ... */ }
protected function afterExecute(): void { /* ... */ }
}
Middleware Use Symfony middleware to intercept saga execution:
// src/Saga/Middleware/LoggingMiddleware.php
use Brzuchal\SagaSymfony\Saga\SagaInterface;
class LoggingMiddleware implements SagaMiddlewareInterface {
public function handle(SagaInterface $saga, callable $next): void {
$this->logger->info('Saga started');
$next();
$this->logger->info('Saga completed');
}
}
Testing
Mock dependencies and use SagaTestCase (if available) or PHPUnit:
public function testSagaExecution() {
$saga = $this->createMock(ExampleSaga::class);
$saga->method('execute')->willReturn(true);
$this->assertTrue($saga->execute());
}
How can I help you explore Laravel packages today?