agluh/outbox-bundle
Laravel outbox pattern bundle for reliable event publishing: capture domain events in an outbox table within your transaction, then dispatch them asynchronously to queues or brokers. Helps avoid dual-write issues and improves consistency between your DB and integrations.
Install the Bundle
composer require agluh/outbox-bundle
Register the bundle in config/bundles.php:
return [
// ...
Agluh\OutboxBundle\AgluhOutboxBundle::class => ['all' => true],
];
Configure the Database
Run migrations to create the outbox_message table:
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
First Use Case: Publishing a Message
Inject the OutboxPublisher service and publish a message:
use Agluh\OutboxBundle\Publisher\OutboxPublisherInterface;
class MyCommand
{
public function __construct(private OutboxPublisherInterface $publisher) {}
public function handle()
{
$this->publisher->publish(
new MyDomainEvent('event_id', ['data' => 'value']),
new \DateTimeImmutable()
);
}
}
config/packages/agluh_outbox.yaml for default settings (e.g., outbox_table, lock_timeout).src/Entity/OutboxMessage.php to understand the schema.Agluh\OutboxBundle\Publisher\OutboxPublisherInterface for method signatures.Publish Events
Use OutboxPublisherInterface to enqueue domain events:
$this->publisher->publish(
new OrderCreatedEvent($orderId, $customerId),
new \DateTimeImmutable()
);
Process Outbox (Cron Job or Symfony Command)
Run the outbox:process command to publish messages to their destinations (e.g., Kafka, RabbitMQ):
php bin/console outbox:process
Configure destinations in agluh_outbox.yaml:
outbox:
destinations:
kafka:
type: kafka
config: '%env(KAFKA_CONFIG)%'
topics: ['events']
Idempotency
Use UUIDs for event IDs and leverage the processed_at field to avoid duplicates.
Symfony Messenger Bridge
Combine with symfony/messenger for async processing:
# config/packages/messenger.yaml
messenger:
transports:
outbox: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
'Agluh\OutboxBundle\Message\ProcessOutboxMessage': outbox
Doctrine Events Auto-publish events after entity persistence:
// src/EventListener/OrderListener.php
public function postPersist(Order $order, LifecycleEventArgs $args)
{
$this->publisher->publish(new OrderCreatedEvent($order->getId()));
}
Testing
Use OutboxPublisherTest (provided by the bundle) to assert published messages:
$this->assertPublished(new OrderCreatedEvent($orderId));
Locking Issues
LockFactory to prevent concurrent processing.lock_timeout in config if processing takes >30s (default):
outbox:
lock_timeout: 60
Transaction Boundaries
publish() is called within a transaction (e.g., after EntityManager::flush()).// ❌ Avoid: No transaction!
$this->publisher->publish($event);
$this->entityManager->flush();
Destination Configuration
DestinationInterface. The bundle ships with Kafka/RabbitMQ adapters.type in destinations config will silently fail. Validate with:
php bin/console debug:config agluh_outbox
Query the Outbox Table Check pending messages:
SELECT * FROM outbox_message WHERE processed_at IS NULL;
Enable Debug Logging
Add to config/packages/dev/agluh_outbox.yaml:
outbox:
debug: true
Manual Processing Force-process a single message (for testing):
php bin/console outbox:process --message-id=UUID_HERE
Custom Destinations Create a new destination class:
use Agluh\OutboxBundle\Destination\DestinationInterface;
class SlackDestination implements DestinationInterface
{
public function publish(array $message): void
{
// Send to Slack webhook
}
}
Register in config:
outbox:
destinations:
slack:
type: slack
config: '%env(SLACK_WEBHOOK)%'
Event Transformers
Override OutboxMessageTransformer to customize serialization:
// src/Outbox/Transformer/CustomTransformer.php
use Agluh\OutboxBundle\Transformer\OutboxMessageTransformerInterface;
class CustomTransformer implements OutboxMessageTransformerInterface
{
public function transform(object $event): array
{
return [
'type' => get_class($event),
'payload' => json_encode($event->toArray()),
];
}
}
Bind in services.yaml:
services:
Agluh\OutboxBundle\Transformer\OutboxMessageTransformerInterface: '@App\Outbox\Transformer\CustomTransformer'
Post-Processing Hooks
Subscribe to outbox.message.processed event:
// src/EventListener/OutboxListener.php
use Agluh\OutboxBundle\Event\MessageProcessedEvent;
public function onMessageProcessed(MessageProcessedEvent $event)
{
// Log or trigger side effects
}
Register as a service tag:
tags:
- { name: kernel.event_listener, event: outbox.message.processed, method: onMessageProcessed }
How can I help you explore Laravel packages today?