ecotone/dbal
Ecotone Doctrine DBAL integration: use your relational database for durable async message transport, outbox, dead letter queue, saga state storage, and JSON document store. Provides transactional message handling and declarative DBAL query “business methods”.
Installation:
composer require ecotone/ecotone
For Laravel, use the Ecotone Lite integration:
composer require ecotone/ecotone-laravel
Configure DBAL Connection:
Add to config/ecotone.php (or publish the config):
'connections' => [
'default' => [
'dbal' => [
'connection' => 'mysql', // or 'pgsql', 'sqlite'
'table_prefix' => 'ecotone_',
],
],
],
Ensure your .env has the correct database credentials.
First Command Handler: Create a simple command and handler:
// app/Commands/ProcessOrder.php
namespace App\Commands;
#[CommandHandler]
class ProcessOrder
{
public function __invoke(ProcessOrderCommand $command)
{
// Business logic here
return new OrderProcessedEvent($command->orderId);
}
}
Dispatch a Command: In a controller or service:
use Ecotone\CommandBus;
public function __construct(private CommandBus $commandBus) {}
public function handleOrder(OrderRequest $request)
{
$command = new ProcessOrderCommand($request->orderId);
$this->commandBus->dispatch($command);
}
Run the Consumer: Start the Ecotone consumer in a terminal:
php artisan ecotone:consume
This replaces queue:work for processing commands.
#[CommandHandler] Attribute: The core entry point for message handling. Explore how it replaces Laravel’s ShouldQueue jobs.Problem: You have a SendWelcomeEmail job that fails intermittently due to SMTP issues.
Solution: Use Ecotone’s outbox pattern to ensure the email is sent atomically with the user creation.
Create a Command:
#[CommandHandler]
class SendWelcomeEmailHandler
{
public function __invoke(SendWelcomeEmailCommand $command)
{
Mail::to($command->email)->send(new WelcomeEmail($command->userId));
return new EmailSentEvent($command->userId);
}
}
Dispatch in User Creation:
public function createUser(UserRequest $request)
{
$user = User::create($request->validated());
$this->commandBus->dispatch(new SendWelcomeEmailCommand(
$user->email,
$user->id
));
}
Configure Outbox:
Ensure your database has the ecotone_messages table (migrated automatically or manually via schema).
Verify Atomicity:
If the system crashes after user creation but before the email is sent, the outbox ensures the email is retried on the next ecotone:consume run.
ShouldQueue jobs with Ecotone’s #[CommandHandler].#[CommandHandler]
class UpdateInventoryHandler
{
public function __invoke(UpdateInventoryCommand $command)
{
// Update inventory in DB
Inventory::where('product_id', $command->productId)
->decrement('stock', $command->quantity);
// Publish an event (optional)
return new InventoryUpdatedEvent($command->productId);
}
}
Repository, Mailer) just like in Laravel controllers.public function createOrder(OrderRequest $request)
{
DB::transaction(function () use ($request) {
$order = Order::create($request->validated());
$this->commandBus->dispatch(new PublishOrderCreatedEventCommand(
$order->id,
$order->items
));
});
}
#[CommandHandler]
class PublishOrderCreatedEventHandler
{
public function __invoke(PublishOrderCreatedEventCommand $command)
{
event(new OrderCreated($command->orderId, $command->items));
}
}
dispatch() for events or use Ecotone’s event bus for consistency.config/ecotone.php:
'dead_letter' => [
'enabled' => true,
'table' => 'ecotone_dead_letters',
],
#[CommandHandler]
class ProcessPaymentHandler
{
public function __invoke(ProcessPaymentCommand $command)
{
if (!$this->paymentGateway->charge($command->amount)) {
throw new PaymentFailedException("Gateway error: " . $this->paymentGateway->getLastError());
}
}
}
ecotone:replay artisan command or manually inspect the ecotone_dead_letters table.#[Saga]
class OrderFulfillmentSaga
{
#[StartSaga]
public function start(OrderCreated $event)
{
return new ReserveInventoryCommand($event->orderId, $event->items);
}
#[SagaMethod]
public function reserveInventory(ReserveInventoryCommand $command, ReserveInventoryResult $result)
{
if (!$result->success) {
throw new InventoryUnavailableException();
}
return new ProcessPaymentCommand($command->orderId, $command->total);
}
#[SagaMethod]
public function processPayment(ProcessPaymentCommand $command, PaymentProcessed $event)
{
return new ShipOrderCommand($command->orderId);
}
}
#[Identifier] (e.g., order_id).#[DocumentStore]
class UserProfile
{
public function __construct(
public string $userId,
public array $preferences,
public ?array $addresses = null
) {}
}
#[DbalBusinessMethod]:
#[DbalBusinessMethod]
public function findByUserId(string $userId): ?UserProfile
{
return $this->findOneBy(['user_id' => $userId]);
}
Service Provider Setup:
Add to AppServiceProvider:
public function boot()
{
$this->app->register(\Ecotone\Laravel\EcotoneServiceProvider::class);
}
Artisan Commands:
php artisan ecotone:consume: Start the message consumer (replaces queue:work).php artisan ecotone:install: Set up DB tables (run once).php artisan ecotone:replay: Replay dead-lettered commands.Queue Workers:
Replace queue:work in your supervisor.conf or queue:listen with:
php artisan ecotone:consume --daemon
CommandBus mock:
$commandBus = new CommandBus();
$handler = new ProcessOrderHandler();
$result = $commandBus->dispatch(new ProcessOrderCommand('123'));
How can I help you explore Laravel packages today?