andreo/eventsauce-outbox
Extended outbox components for EventSauce on PHP 8.2+. Includes an outbox-aware aggregate repository, a forwarding message consumer to dispatch to your queue, and a Symfony Console command to consume relays with batch/commit, sleep, and limit options.
Install the Package
composer require andreo/eventsauce-outbox
Note: Requires PHP 8.2+ and Symfony Console (install via composer require symfony/console).
Register EventSauce Dependencies
Configure EventSauce’s core components (MessageRepository, AggregateRootRepository) in Laravel’s config/app.php or a service provider:
$this->app->bind(EventSauce\EventSourcing\MessageRepository::class, function ($app) {
return new EventSauce\EventSourcing\MessageRepository(
// Your EventSauce config
);
});
Create a Custom Outbox Repository Extend the package’s repository to work with Laravel’s container:
use Andreo\EventSauce\Outbox\Repository\EventSourcedAggregateRootRepositoryForOutbox;
use Illuminate\Contracts\Container\Container;
$repository = new EventSourcedAggregateRootRepositoryForOutbox(
aggregateRootClassName: MyAggregate::class,
messageRepository: $this->app->make(EventSauce\EventSourcing\MessageRepository::class),
regularRepository: $this->app->make(EventSauce\EventSourcing\AggregateRootRepository::class),
container: $this->app // Laravel's container for service resolution
);
First Use Case: Dispatch Events to a Queue
Use the ForwardingMessageConsumer to forward events to Laravel’s queue system:
use Andreo\EventSauce\Outbox\MessageConsumer\ForwardingMessageConsumer;
use EventSauce\EventSourcing\MessageDispatcher;
$dispatcher = new ForwardingMessageConsumer(
messageDispatcher: $this->app->make(MessageDispatcher::class)
);
Run the Outbox Consumer via Artisan Create a Laravel Artisan command to replace the Symfony command:
php artisan make:command ProcessOutboxMessages
Configure it to use the package’s OutboxMessagesConsumeCommand logic:
use Andreo\EventSauce\Outbox\Command\OutboxMessagesConsumeCommand;
use Symfony\Component\DependencyInjection\ServiceLocator;
protected function handle(): void {
$relays = new ServiceLocator(); // Replace with Laravel-compatible service locator
$relays->set('foo-relay-id', $this->app->make(\EventSauce\MessageOutbox\OutboxRelay::class));
$command = new OutboxMessagesConsumeCommand(
relays: $relays,
logger: $this->app->make(\Psr\Log\LoggerInterface::class)
);
$command->run(
relays: ['foo-relay-id'],
run: true,
batchSize: 100,
commitSize: 1,
sleep: 1,
limit: -1
);
}
Run it with:
php artisan process:outbox-messages
Event Production
When an aggregate root emits events, they are stored in EventSauce’s MessageRepository and automatically enqueued in the outbox (via the custom repository).
$aggregate = $repository->retrieve(MyAggregate::class, $id);
$aggregate->apply(new MyEvent());
$repository->persist($aggregate); // Events are outboxed here
Asynchronous Processing
Use Laravel’s queue system to process outboxed messages. Create a queue job to handle the ForwardingMessageConsumer:
use EventSauce\EventSourcing\Message;
class DispatchOutboxMessagesJob implements ShouldQueue {
public function handle(): void {
$consumer = new ForwardingMessageConsumer(
messageDispatcher: $this->app->make(MessageDispatcher::class)
);
$consumer->consume(); // Processes messages from the outbox
}
}
Dispatch this job periodically via a Laravel scheduler:
$schedule->command('process:outbox-messages')->everyMinute();
Batch Processing
Configure the OutboxMessagesConsumeCommand with optimal batch sizes for your queue system:
$command->run(
batchSize: 50, // Fetch 50 messages at once
commitSize: 10 // Commit 10 messages as a transaction
);
To forward outboxed messages to Laravel’s queue, create a custom MessageDispatcher adapter:
use EventSauce\EventSourcing\MessageDispatcher;
use Illuminate\Bus\Dispatcher as LaravelDispatcher;
use Illuminate\Contracts\Queue\ShouldQueue;
class LaravelQueueMessageDispatcher implements MessageDispatcher {
public function __construct(
private LaravelDispatcher $laravelDispatcher
) {}
public function dispatch(Message $message): void {
$this->laravelDispatcher->dispatch(
new DispatchEventJob($message)
);
}
}
Define relays in Laravel’s service container (e.g., config/eventsauce.php):
'relays' => [
'foo-relay-id' => [
'class' => \EventSauce\MessageOutbox\OutboxRelay::class,
'config' => [
'queue' => 'events',
'connection' => 'redis'
]
]
],
Ensure outbox operations are transactional. Use Laravel’s database transactions with EventSauce’s:
use Illuminate\Support\Facades\DB;
DB::transaction(function () use ($repository, $aggregate) {
$aggregate->apply(new MyEvent());
$repository->persist($aggregate); // Outboxed within this transaction
});
Symfony Dependency Overhead
ServiceLocator and Console. To avoid tight coupling:
ServiceLocator with Laravel’s container or a custom wrapper:
use Illuminate\Support\ServiceLocator as LaravelServiceLocator;
$locator = new LaravelServiceLocator();
$locator->add('foo-relay-id', $this->app->make(\EventSauce\MessageOutbox\OutboxRelay::class));
Transaction Conflicts
MessageRepository commits before Laravel’s transaction ends.InnoDB).Queue Serialization
Message objects. Use a serializer adapter:
use EventSauce\EventSourcing\Message;
use Illuminate\Contracts\Queue\ShouldQueue;
class DispatchEventJob implements ShouldQueue {
public function __construct(private Message $message) {}
public function handle(): void {
// Deserialize and process the message
}
public function serialize(): string {
return serialize([
'event' => $this->message->event(),
'metadata' => $this->message->metadata(),
]);
}
public static function deserialize($data): self {
$data = unserialize($data);
return new self(
new Message(
$data['event'],
$data['metadata']
)
);
}
}
Outbox Table Schema
Schema::create('outbox_messages', function (Blueprint $table) {
$table->id();
$table->string('aggregate_id');
$table->string('aggregate_type');
$table->json('event');
$table->json('metadata');
$table->boolean('published')->default(false);
$table->timestamps();
});
Relay Id Mismatches
OutboxMessagesConsumeCommand requires relay IDs registered in the ServiceLocator. If using Laravel’s container:
$this->app->bind(\EventSauce\MessageOutbox\OutboxRelay::class, function ($app) {
return new \EventSauce\MessageOutbox\OutboxRelay(
// Your relay config
);
});
ForwardingMessageConsumer to track failed dispatches:
use Psr\Log\LoggerInterface;
$consumer = new ForwardingMessageConsumer(
messageDispatcher: $dispatcher,
logger: $this->app->make(LoggerInterface::class)
);
How can I help you explore Laravel packages today?