babelqueue / php-sdk
Framework-agnostic core for BabelQueue: the canonical polyglot queue envelope codec, contracts and dead-letter helpers. Framework adapters (Laravel, Symfony, ...) are built on top.
Fund package maintenance!
Requires
- php: ^8.2
- ext-json: *
Requires (Dev)
- mockery/mockery: ^1.6
- open-telemetry/api: ^1.9
- open-telemetry/sdk: ^1.14
- php-amqplib/php-amqplib: ^3.5
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.5|^11.0
- predis/predis: ^2.2
Suggests
- ext-rdkafka: To produce to Apache Kafka (the §6 PHP path) via KafkaTransport (the php-rdkafka PECL extension over librdkafka; opt-in, relaxes GR-7 for Kafka — ADR-0019).
- ext-redis: Use the phpredis extension with your own one-method Transport, or install predis/predis for the bundled RedisTransport.
- aws/aws-sdk-php: For the framework-less Amazon SQS transport (BabelQueue\Transport\SqsTransport).
- open-telemetry/api: To emit OpenTelemetry produce/consume spans via BabelQueue\Otel\Tracing (opt-in, ADR-0025; also install open-telemetry/sdk to export).
- php-amqplib/php-amqplib: For the framework-less RabbitMQ transport (BabelQueue\Transport\AmqpTransport).
- predis/predis: Pure-PHP Redis client for the framework-less Redis transport (BabelQueue\Transport\RedisTransport).
- stomp-php/stomp-php: To produce to Apache ActiveMQ Artemis over STOMP (the §7 PHP path) via StompTransport.
- textalk/websocket: Pure-PHP WebSocket client to produce to Apache Pulsar (the §5 PHP path) via PulsarTransport over Pulsar's WebSocket API (GR-7 intact — ADR-0020).
README
The framework-agnostic core of BabelQueue for PHP — the canonical polyglot queue envelope codec, contracts and dead-letter helpers. Framework adapters (
babelqueue/laravel,babelqueue/symfony, …) are built on top of this.
You usually don't install this directly — you install an adapter:
composer require babelqueue/laravel # Laravel composer require babelqueue/symfony # Symfony (Messenger)
…and the adapter pulls this core in. Install it directly only for a framework-less PHP app, or to build a new adapter.
composer require babelqueue/php-sdk
What's in here
| Area | Class | Role |
|---|---|---|
| Codec | BabelQueue\Codec\EnvelopeCodec |
Build / encode / decode the canonical {job, trace_id, data, meta, attempts} envelope (schema_version 1). The single PHP implementation of the wire format — every adapter reuses it, so Laravel and Symfony can't drift. |
| Contracts | BabelQueue\Contracts\PolyglotJob |
Producible message: getBabelUrn() + toPayload(). |
BabelQueue\Contracts\HasBabelUrn / HasTraceId |
URN identity / optional trace-id propagation. | |
BabelQueue\Contracts\InboundMessage |
Read-only decoded view of a consumed envelope. | |
BabelQueue\Contracts\Transport |
Minimal publish seam (framework-less / adapter use). | |
| Validation | BabelQueue\Validation\EnvelopeValidator |
Consumer-side validation with a reason — quarantine an unsupported meta.schema_version instead of dropping it. |
| Transports | BabelQueue\Transport\RedisTransport / AmqpTransport |
Optional framework-less reference Transport impls (Redis RPUSH; RabbitMQ durable + contract AMQP properties). |
| Dead-letter | BabelQueue\DeadLetter\DeadLetter |
Annotate an envelope with the additive dead_letter block (ADR-0009). |
| Outbox | BabelQueue\Outbox\Outbox / OutboxRelay / OutboxStore |
Transactional outbox (ADR-0029): persist the message atomically with the business write, relay it later. Dependency-free — OutboxStore is an interface you bind to your DB. |
| Routing | BabelQueue\Routing\UnknownUrnStrategy |
fail / delete / release / dead_letter constants. |
| Support | BabelQueue\Support\Uuid |
Dependency-free UUIDv4 (no ramsey/symfony-uid needed). |
| Errors | BabelQueue\Exceptions\BabelQueueException / UnknownUrnException / InvalidEnvelopeException |
Exception hierarchy; InvalidEnvelopeException carries the rejection reason + envelope. |
The contract this core implements — the canonical envelope, URN scheme, broker
bindings and versioning policy — is documented at
babelqueue.com. The golden conformance fixtures live in
tests/fixtures/ — every PHP package must round-trip them.
Framework-less use
Produce the canonical envelope from a plain PHP app and let any other SDK consume it. The reference transports keep the core dependency-free — install only the broker client you use:
composer require predis/predis # for RedisTransport composer require php-amqplib/php-amqplib # for AmqpTransport
use BabelQueue\Codec\EnvelopeCodec; use BabelQueue\Transport\RedisTransport; use BabelQueue\Validation\EnvelopeValidator; // Produce — a Go/Python/Node consumer reads the identical envelope off "orders". $transport = new RedisTransport(new Predis\Client('redis://localhost:6379')); $transport->publish(EnvelopeCodec::encode(EnvelopeCodec::fromJob($job, 'orders')), 'orders'); // Consume (your own loop) — validate before dispatch, quarantine the unknown. $envelope = EnvelopeCodec::decode($rawBody); if ($reason = EnvelopeValidator::check($envelope)) { // $reason === 'unsupported_schema_version' → dead-letter, don't drop. return; }
phpredis (ext-redis) users can implement the one-method Transport directly —
it is just an rpush.
Transactional outbox (ADR-0029)
A plain producer makes a dual write — commit the business row and publish to the
broker — two systems that can disagree on a crash. The outbox removes it: the message is
written into the same database, in the same transaction as the business data (so they
commit or roll back atomically), and a separate relay publishes it afterwards. No
distributed transaction; exactly-once handoff into the broker (then at-least-once on the
wire, deduped on meta.id by the consumer-side Idempotent::wrap, ADR-0022).
The helper is dependency-free (GR-7): the core defines OutboxStore and you bind it to
your DB. The transaction boundary is yours — OutboxStore::save() runs inside the
transaction you opened around your business write. The envelope is stored verbatim
(GR-1) and relayed byte-for-byte, so trace_id is preserved end-to-end (GR-4).
use BabelQueue\Codec\EnvelopeCodec; use BabelQueue\Outbox\Outbox; use BabelQueue\Outbox\OutboxRelay; // WRITE side — one transaction for the business row AND the message (your tx boundary). $outbox = new Outbox($store); // $store implements OutboxStore (your DB) $db->transaction(function () use ($db, $outbox, $order): void { $db->insertOrder($order); // business write $outbox->write(EnvelopeCodec::make('urn:babel:orders:created', $order, 'orders')); }); // both commit, or neither // READ side — a worker/cron drains the durable rows onto the broker. $relay = new OutboxRelay($transport, $store); // $transport is any BabelQueue Transport $relay->drain(); // publishes verbatim, marks published/failed
OutboxStore is four methods — save(), fetchUnpublished(), markPublished(),
markFailed(). A reference InMemoryOutboxStore ships for tests; a runnable
InitORM-backed adapter + the outbox-table DDL live in
babelqueue-examples/outbox-initorm/,
keeping this core DB-free.
Design
This core is the contract runtime, not a worker. It does not own a broker loop or retry — adapters bind to each framework's native queue (Laravel's drop-in driver, Symfony Messenger) and reuse that framework's worker/retry.
Testing
composer install vendor/bin/phpunit
License
MIT © Muhammet Şafak.