定制 webrek/laravel-outbox 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

webrek/laravel-outbox

Composer 安装命令:

composer require webrek/laravel-outbox

包简介

Transactional outbox for Laravel: stage messages inside your database transaction and relay them reliably with retries and backoff.

README 文档

README

Latest Version on Packagist Total Downloads Tests PHP Version License

A transactional outbox for Laravel. Stage a message inside the same database transaction as your business write, and a relay delivers it afterwards with retries and backoff. The write and the message commit together — either both land or neither does — so you never publish an event for a change that rolled back, and you never lose an event for a change that committed.

This is the producer half of exactly-once. Pair it with webrek/laravel-idempotency on the consumer to get end-to-end exactly-once effects over at-least-once infrastructure.

Why

Dispatching a queued job, firing a webhook, or publishing to a broker after saving a model is a dual-write: if the process dies between the commit and the dispatch, the side effect is lost. Doing it before the commit is worse — the effect fires even if the transaction rolls back. The outbox pattern removes the gap by writing the intent to the same database, in the same transaction, and delivering it from there.

use Illuminate\Support\Facades\DB;
use Webrek\Outbox\Facades\Outbox;

DB::transaction(function () use ($request) {
    $order = Order::create($request->validated());

    // Commits atomically with the order. No order, no message — and vice versa.
    Outbox::publish('order.placed', ['order_id' => $order->id]);
});

Install

composer require webrek/laravel-outbox

Publish and run the migration:

php artisan vendor:publish --tag=outbox-migrations
php artisan migrate

Optionally publish the config:

php artisan vendor:publish --tag=outbox-config

The outbox table must live on the same connection as the data you stage messages alongside — atomicity only spans a single connection's transaction. Set outbox.connection accordingly (it defaults to your default connection).

Relaying messages

Run the relay as a long-lived worker (like queue:work):

php artisan outbox:work

It claims due messages with a row lock — safe to run several workers in parallel — hands each to a publisher, and marks it published. A failed delivery is retried with exponential backoff up to max_attempts, after which the message is discarded. A message left processing by a crashed worker is reclaimed once claim_timeout passes.

Process a single batch and exit (handy for scheduling or tests):

php artisan outbox:work --once

Trim delivered messages on a schedule:

use Illuminate\Support\Facades\Schedule;

Schedule::command('outbox:prune')->daily();   // keeps the last `prune.retention_hours`

Delivering messages

How a message reaches the outside world is up to a publisher. Out of the box the package ships EventPublisher, which turns every message into an OutboxMessageReady event you can listen for:

use Webrek\Outbox\Events\OutboxMessageReady;

Event::listen(OutboxMessageReady::class, function (OutboxMessageReady $event) {
    $message = $event->message;

    Http::post('https://example.test/hooks', $message->payload)->throw();
});

Delivery is synchronous: if the listener throws, the message is rescheduled; if it returns, the message is marked published.

Prefer a dedicated class? Implement the contract and point the config at it:

use Webrek\Outbox\Contracts\Publisher;
use Webrek\Outbox\Models\OutboxMessage;

class BrokerPublisher implements Publisher
{
    public function publish(OutboxMessage $message): void
    {
        // push to Kafka / RabbitMQ / SNS / an HTTP endpoint…
        // throw to retry, return to acknowledge.
    }
}
// config/outbox.php
'publisher' => App\Outbox\BrokerPublisher::class,

Observability

The relay fires lifecycle events you can hook into for metrics and alerting:

Event When
OutboxMessagePublished A message was delivered successfully.
OutboxMessageFailed An attempt failed; the message will be retried.
OutboxMessageDiscarded The retry budget was exhausted; the message is given up on.

Each carries the OutboxMessage; the failure events also carry the Throwable.

Recovering discarded messages

A message that exhausts its retry budget is marked failed and left in the table for inspection — never silently dropped. Once you have fixed the downstream, reset messages back to pending so the relay tries them again with a fresh budget:

php artisan outbox:retry --all          # every discarded message
php artisan outbox:retry <id> <id># specific messages

To spread the retries of a large backlog so they do not all fire at once, raise retry.jitter (0–1) before reprocessing.

Inspecting the outbox

See how many messages sit in each state — and how stale the oldest pending one is — at a glance:

php artisan outbox:status

Faking it in tests

Outbox::fake() swaps the outbox for an in-memory recorder, so your application tests can assert what would be published without writing to the database or running the relay:

use Webrek\Outbox\Facades\Outbox;

Outbox::fake();

$this->post('/orders', [...]);

Outbox::assertPublished('order.placed', fn ($message) => $message->payload['id'] === $order->id);
Outbox::assertPublishedTimes('order.placed', 1);
Outbox::assertNothingPublished();   // or assert nothing leaked

Configuration

return [
    'connection' => env('OUTBOX_CONNECTION'),   // same connection as your business data
    'table' => 'outbox_messages',
    'publisher' => Webrek\Outbox\Publishers\EventPublisher::class,
    'max_attempts' => 10,                        // attempts before discarding
    'batch_size' => 100,                         // messages claimed per relay pass
    'claim_timeout' => 300,                       // seconds before a stuck message is reclaimed
    'retry' => [
        'base_seconds' => 10,                     // delay = base * multiplier^(attempt - 1)
        'max_seconds' => 3600,
        'multiplier' => 2,
        'jitter' => 0.0,                          // 0–1: spread retries to avoid a thundering herd
    ],
    'prune' => [
        'retention_hours' => 168,
    ],
];

Requirements

Component Version
PHP 8.2+
Laravel 12.x / 13.x
Database Any with transactions (PostgreSQL, MySQL/MariaDB, SQLite, SQL Server)

Testing

composer install
composer test

Contributing

See CONTRIBUTING.md.

Security

Please review the security policy before reporting a vulnerability.

License

Released under the MIT license.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 6
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-17