PHPackages                             sumantasam1990/phpoutbox - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. sumantasam1990/phpoutbox

ActiveLibrary[Queues &amp; Workers](/categories/queues)

sumantasam1990/phpoutbox
========================

Transactional Outbox Pattern for PHP — guaranteed at-least-once event delivery for Laravel, Symfony, and vanilla PHP

130[1 issues](https://github.com/sumantasam1990/PHPOutbox/issues)PHPCI failing

Since Apr 8Pushed 2mo agoCompare

[ Source](https://github.com/sumantasam1990/PHPOutbox)[ Packagist](https://packagist.org/packages/sumantasam1990/phpoutbox)[ RSS](/packages/sumantasam1990-phpoutbox/feed)WikiDiscussions main Synced 2w ago

READMEChangelogDependenciesVersions (1)Used By (0)

PHPOutbox — Transactional Outbox for PHP
========================================

[](#phpoutbox--transactional-outbox-for-php)

[![PHP 8.2+](https://camo.githubusercontent.com/0f16581d1180dbfd4c0e13166ec1267d4ad2f2fab8281ea6d6b284cf5c65d921/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e322532422d626c75652e737667)](https://www.php.net/)[![License: MIT](https://camo.githubusercontent.com/784362b26e4b3546254f1893e778ba64616e362bd6ac791991d2c9e880a3a64e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e2e737667)](LICENSE)[![PHPStan Level 8](https://camo.githubusercontent.com/c51bda247654363d3e30bc352674dd761a9557803a14af0226eb411d6dc0006b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d4c6576656c253230382d627269676874677265656e2e737667)](phpstan.neon)

**Stop losing events.** PHPOutbox implements the [Transactional Outbox Pattern](https://microservices.io/patterns/data/transactional-outbox.html) for PHP — guaranteed at-least-once event delivery for Laravel, Symfony, and vanilla PHP applications.

The Problem
-----------

[](#the-problem)

```
// ❌ DANGEROUS — dual-write problem
DB::transaction(function () use ($order) {
    $order->save();
});
// If the app crashes here, or the queue is down...
event(new OrderCreated($order));  // 💀 Event lost forever
```

The Solution
------------

[](#the-solution)

```
// ✅ SAFE — atomic outbox write
DB::transaction(function () use ($order) {
    $order->save();
    Outbox::store('Order', $order->id, 'OrderCreated', $order->toArray());
    // Both written in the SAME transaction — both succeed or both fail
});

// Background relay publishes to your queue — guaranteed delivery
// php artisan outbox:relay
```

Features
--------

[](#features)

- **Atomic writes** — Events stored in the same DB transaction as business data
- **Background relay** — Polls outbox table, publishes to your queue
- **Retry with backoff** — Failed publishes retry automatically
- **Dead letter queue** — Messages moved to dead-letter after max retries
- **Concurrent workers** — Multiple relays via `SELECT FOR UPDATE SKIP LOCKED`
- **Framework-agnostic** — Core works with raw PDO, zero framework deps
- **Laravel adapter** — ServiceProvider, Facade, Artisan commands
- **Symfony adapter** — Bundle, Console commands, Messenger integration
- **Observability** — PSR-3 logging, relay metrics per cycle
- **Housekeeping** — Auto-prune old messages

Requirements
------------

[](#requirements)

- PHP 8.2+
- MySQL 8.0+ or PostgreSQL 9.5+ (SQLite for testing)
- PDO extension

Installation
------------

[](#installation)

```
composer require sumantasam1990/phpoutbox
```

Quick Start
-----------

[](#quick-start)

### Laravel

[](#laravel)

**1. Publish config:**

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

**2. Create the outbox table:**

```
php artisan outbox:migrate
```

**3. Store events (inside your DB transaction):**

```
use PhpOutbox\Outbox\Laravel\Facades\Outbox;
use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    $order = Order::create([
        'customer_id' => 42,
        'total' => 199.99,
    ]);

    Outbox::store(
        aggregateType: 'Order',
        aggregateId: (string) $order->id,
        eventType: 'OrderCreated',
        payload: $order->toArray(),
        headers: ['correlation-id' => request()->header('X-Correlation-ID')],
    );
});
```

**4. Run the relay daemon:**

```
php artisan outbox:relay
```

Or for cron-based relay:

```
php artisan outbox:relay --once
```

**5. Schedule pruning in `routes/console.php`:**

```
Schedule::command('outbox:prune --days=30')->daily();
```

### Symfony

[](#symfony)

**1. Register the bundle:**

```
// config/bundles.php
return [
    // ...
    PhpOutbox\Outbox\Symfony\OutboxBundle::class => ['all' => true],
];
```

**2. Configure:**

```
# config/packages/outbox.yaml
outbox:
    table_name: outbox_messages
    relay:
        batch_size: 100
        poll_interval_ms: 1000
        max_attempts: 5
```

**3. Run the relay:**

```
bin/console outbox:relay
```

### Vanilla PHP (No Framework)

[](#vanilla-php-no-framework)

```
use PhpOutbox\Outbox\Outbox;
use PhpOutbox\Outbox\OutboxConfig;
use PhpOutbox\Outbox\Store\PdoOutboxStore;
use PhpOutbox\Outbox\Store\Schema;
use PhpOutbox\Outbox\Relay\OutboxRelay;

// Setup
$pdo = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'pass');
$config = new OutboxConfig(batchSize: 50, maxAttempts: 3);
$store = new PdoOutboxStore($pdo, $config);
$outbox = new Outbox($store);

// Create table (one-time)
$pdo->exec(Schema::mysql());

// Store an event (inside your transaction)
$pdo->beginTransaction();
$pdo->exec("INSERT INTO orders (id, total) VALUES (1, 99.99)");
$outbox->store('Order', '1', 'OrderCreated', ['total' => 99.99]);
$pdo->commit();

// Run relay (implement OutboxPublisher for your broker)
$publisher = new MyRabbitMQPublisher();
$relay = new OutboxRelay($store, $publisher, $config);
$relay->run(); // Blocks forever — run in a supervisor
```

Configuration
-------------

[](#configuration)

### Laravel Config (`config/outbox.php`)

[](#laravel-config-configoutboxphp)

KeyEnv VariableDefaultDescription`table_name``OUTBOX_TABLE``outbox_messages`Outbox table name`connection``OUTBOX_CONNECTION``null` (default)Database connection`relay.batch_size``OUTBOX_BATCH_SIZE``100`Messages per relay cycle`relay.poll_interval_ms``OUTBOX_POLL_INTERVAL``1000`Ms between polls`relay.max_attempts``OUTBOX_MAX_ATTEMPTS``5`Max retries before dead-letter`publisher.queue_connection``OUTBOX_QUEUE_CONNECTION``null`Queue connection`publisher.queue_name``OUTBOX_QUEUE_NAME``outbox`Queue name`prune_after_days``OUTBOX_PRUNE_DAYS``30`Days to keep published messages`delete_on_publish``OUTBOX_DELETE_ON_PUBLISH``false`Delete vs mark as published`id_generator``OUTBOX_ID_GENERATOR``uuid7`ID strategy: `uuid7` or `ulid`Custom Publisher
----------------

[](#custom-publisher)

Implement `OutboxPublisher` for your message broker:

```
use PhpOutbox\Outbox\Contracts\OutboxPublisher;
use PhpOutbox\Outbox\Exception\PublishException;
use PhpOutbox\Outbox\Message\OutboxMessage;

class RabbitMQPublisher implements OutboxPublisher
{
    public function __construct(private AMQPChannel $channel) {}

    public function publish(OutboxMessage $message): void
    {
        try {
            $this->channel->basic_publish(
                new AMQPMessage($message->payload),
                'events',
                $message->eventType,
            );
        } catch (\Throwable $e) {
            throw PublishException::failed($message->id, $e);
        }
    }

    public function publishBatch(array $messages): void
    {
        foreach ($messages as $message) {
            $this->publish($message);
        }
    }
}
```

Monitoring
----------

[](#monitoring)

The relay returns metrics per cycle:

```
$metrics = $relay->runOnce();

echo $metrics->summary();
// "Cycle #42: 100 processed (98 published, 1 failed, 1 dead-lettered) in 45.2ms"

$metrics->processed;    // Total messages handled
$metrics->published;    // Successfully published
$metrics->failed;       // Failed (will retry)
$metrics->deadLettered; // Exhausted retries
$metrics->durationMs;   // Cycle duration
```

Architecture
------------

[](#architecture)

See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for detailed flow diagrams, concurrency model, and extension points.

Testing
-------

[](#testing)

```
# All tests
./vendor/bin/phpunit

# Unit tests only
./vendor/bin/phpunit --testsuite=unit

# Integration tests (SQLite in-memory)
./vendor/bin/phpunit --testsuite=integration

# Static analysis
./vendor/bin/phpstan analyse
```

Contributing
------------

[](#contributing)

Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct, how to submit pull requests, and our development process.

1. Fork the repository
2. Create a feature branch
3. Write tests for your changes
4. Ensure all tests pass and PHPStan is clean
5. Submit a pull request

License
-------

[](#license)

MIT License. See [LICENSE](LICENSE) for details.

###  Health Score

20

—

LowBetter than 13% of packages

Maintenance55

Moderate activity, may be stable

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity12

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/3205884fbc1e8443b71138d75559fb4a10c81f8ba24d29e0a607b61303b07e98?d=identicon)[sumantasam1990](/maintainers/sumantasam1990)

---

Top Contributors

[![sumantasam1990](https://avatars.githubusercontent.com/u/18149128?v=4)](https://github.com/sumantasam1990 "sumantasam1990 (5 commits)")

---

Tags

event-drivenlaravelmicroserviceoutbox-patternphp8symfony

### Embed Badge

![Health badge](/badges/sumantasam1990-phpoutbox/health.svg)

```
[![Health](https://phpackages.com/badges/sumantasam1990-phpoutbox/health.svg)](https://phpackages.com/packages/sumantasam1990-phpoutbox)
```

###  Alternatives

[league/geotools

Geo-related tools PHP 7.3+ library

1.4k5.5M29](/packages/league-geotools)[illuminate/bus

The Illuminate Bus package.

6045.5M507](/packages/illuminate-bus)[uecode/qpush-bundle

Asynchronous processing for Symfony using Push Queues

1672.5M2](/packages/uecode-qpush-bundle)[jayazhao/think-queue-rabbitmq

为 ThinkPHP5.1 队列增加 RabbitMQ 驱动

141.5k](/packages/jayazhao-think-queue-rabbitmq)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
