winglet / outbox
Winglet-specific transactional outbox package with Doctrine persistence
Requires
- php: ^8.4
- doctrine/orm: ^3.6
- psr/clock: ^1.0
- psr/container: ^2.0
- psr/log: ^3.0
- winglet/core: ^0.12
Requires (Dev)
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^12.5
README
Winglet-specific optional transactional outbox package. The package intentionally includes its Doctrine persistence implementation in the same Composer package to keep installation, smoke testing and releases simple.
Scope
- persists Winglet
DomainEventInterfaceevents in the same Doctrine transaction as application state - unique event id and durable JSON payload/metadata
- MariaDB-safe batch claiming using
FOR UPDATE SKIP LOCKED - configurable retry schedule
- stale processing claim recovery
- transport abstraction
- explicit development logging transport
- CLI inspect, dispatch and recovery commands
- readiness probe and Doctrine mapping through
WingletExtensionInterface
Installation
composer require winglet/outbox
Load the package config and enable the extension:
return [
'app' => [
'extensions' => [
\Winglet\Outbox\Bridge\OutboxExtension::class,
],
],
'outbox' => [
'enabled' => true,
'dispatch' => [
// none = persist only; outbox:dispatch refuses to claim messages
// log = development/smoke-test transport
'transport' => 'none',
],
],
];
Run resources/migrations/001_winglet_outbox.sql through the application's normal migration process.
Transaction semantics
Applications publish through the Core interface:
final class ConfirmRegistrationService
{
public function __construct(private EventPublisherInterface $events) {}
public function confirm(): void
{
// Called inside the application's Doctrine transaction.
$this->events->publish(new RegistrationConfirmed(...));
}
}
TransactionalOutboxEventPublisher calls Doctrine persist() but deliberately does not flush. The application transaction owns flushing and commit. A rollback therefore rolls back both domain mutations and the outbox row.
publish() means accepted for durable asynchronous publication. It does not mean an external consumer has already received the event.
Commands
winglet outbox:inspect all 50
winglet outbox:dispatch 100 worker-1
winglet outbox:recover-stale 100
Transports
The package includes only:
none: persistence without delivery; dispatch command aborts before claiminglog: development/smoke-test delivery that records event identifiers and marks delivery successful; rejected whenapp.env=prod
Production transports implement OutboxTransportInterface and are registered by the consuming application or another optional Winglet package.
Webhook delivery may later be implemented as an optional outbox transport. It is not part of this package.