1. Go to this page and download the library: Download gpalyan/laravel-outbox library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
use TransactionalOutbox\Data\OutboxDraft;
OutboxMessage::storeBatch([
new OutboxDraft(
channel: 'orders.created',
payload: json_encode($order1),
deduplicationKey: "order.created:{$order1->id}",
),
new OutboxDraft(
channel: 'orders.updated',
payload: json_encode($order2),
deduplicationKey: "order.updated:{$order2->id}:{$order2->version}",
),
]);
namespace App\Messaging;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Wire\AMQPTable;
use RuntimeException;
use TransactionalOutbox\Contracts\OutboxPublisherInterface;
use TransactionalOutbox\Models\OutboxMessage;
final class RabbitMqOutboxPublisher implements OutboxPublisherInterface
{
public function __construct(private AMQPStreamConnection $connection) {}
public function publish(OutboxMessage $message): void
{
$route = config("rabbitmq.routes.{$message->channel}");
if (! $route) {
throw new RuntimeException("No RabbitMQ route configured for channel '{$message->channel}'");
}
$amqpMessage = new AMQPMessage(
$message->payload,
[
'content_type' => 'application/octet-stream',
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
'message_id' => $message->id,
'application_headers' => new AMQPTable($message->headers ?? []),
],
);
$channel = $this->connection->channel();
$channel->basic_publish(
msg: $amqpMessage,
exchange: $route['exchange'],
routing_key: $route['routing_key'],
);
// Any AMQP exception here propagates up; the worker catches it and reschedules with backoff.
}
}
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('transactional-outbox:process outbox')
->everyMinute()
->withoutOverlapping();
namespace App\Console\Commands;
use Illuminate\Console\Command;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
use Throwable;
use TransactionalOutbox\Events\MessageConsumed;
final class RabbitMqListenCommand extends Command
{
protected $signature = 'app:rabbitmq-listen {queue} {channel}';
public function handle(AMQPStreamConnection $connection): int
{
$amqpChannel = $connection->channel();
$amqpChannel->basic_qos(prefetch_size: 0, prefetch_count: 10, a_global: false);
$amqpChannel->basic_consume(
queue: $this->argument('queue'),
consumer_tag: '',
no_local: false,
no_ack: false, // <- manual ack; critical for at-least-once
exclusive: false,
nowait: false,
callback: function (AMQPMessage $msg) {
try {
event(new MessageConsumed(
channel: $this->argument('channel'),
payload: $msg->getBody(),
deduplicationKey: $msg->get('message_id'), // broker message id
headers: $this->extractHeaders($msg),
));
$msg->ack();
} catch (Throwable $e) {
// event() failed (DB unavailable, etc.) — return the message to the broker.
$msg->reject(requeue: true);
report($e);
}
},
);
while ($amqpChannel->is_consuming()) {
$amqpChannel->wait();
}
return self::SUCCESS;
}
private function extractHeaders(AMQPMessage $msg): array
{
$headers = $msg->get_properties()['application_headers'] ?? null;
return $headers?->getNativeData() ?? [];
}
}
use TransactionalOutbox\Contracts\InboxHandlerInterface;
use TransactionalOutbox\Models\InboxMessage;
final class PaymentProcessedHandler implements InboxHandlerInterface
{
public function handle(InboxMessage $message): void
{
$data = json_decode($message->payload, true, flags: JSON_THROW_ON_ERROR);
// process $data... throw on failure — the job will retry automatically
}
}