hollyphat / event-bus
Shared Redis Streams event bus for distributed Laravel applications.
Requires
- php: ^8.2
- illuminate/console: ^12.0
- illuminate/contracts: ^12.0
- illuminate/redis: ^12.0
- illuminate/support: ^12.0
Requires (Dev)
- orchestra/testbench: ^10.0
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Shared Redis Streams event bus for distributed Laravel applications.
One stream (name configurable, defaults to events). Each backend runs events:consume with its own consumer group. Every group sees every message; each app only handles events it registered. Data-sync catalogs are added later in those apps — this package is only the transport.
Install
composer require hollyphat/event-bus php artisan vendor:publish --tag=event-bus-config
Env (same Redis for all apps)
Per-app REDIS_PREFIX would hide keys from each other. This package uses connection events with an empty prefix.
PLATFORM_EVENTS_ENABLED=false
PLATFORM_EVENTS_SOURCE=app-a
PLATFORM_EVENTS_GROUP=app-a
PLATFORM_EVENTS_STREAM=events
PLATFORM_REDIS_HOST=127.0.0.1
PLATFORM_REDIS_PORT=6379
PLATFORM_REDIS_PASSWORD=
PLATFORM_REDIS_DB=2
PLATFORM_EVENTS_SOURCE and PLATFORM_EVENTS_GROUP should be set to a unique name per app (e.g. its own slug). GROUP must match the send($target, ...) name other services use.
PLATFORM_EVENTS_STREAM names the Redis stream all apps share — every app talking to each other must set it to the same value. It defaults to events; set it explicitly if you want a project-specific name (e.g. myapp:events).
Leave PLATFORM_EVENTS_ENABLED=false until a sync plan is implemented. sendAndWait throws if publishing is off; broadcast / send no-op.
Receive — $listen on this backend only
This map is not global. It is “events this process should run.” Other backends have their own provider. Add a row when this app should handle a new event.
namespace App\Providers; use App\Events\HandlePayrollDisbursementBatch; use Hollyphat\EventBus\ListenerServiceProvider; class EventListenerServiceProvider extends ListenerServiceProvider { protected array $listen = [ // 'payroll.disbursement.batch' => [ // HandlePayrollDisbursementBatch::class, // ], ]; }
Register the provider in bootstrap/providers.php. Each handler implements Hollyphat\EventBus\Contracts\HandlesEvent:
public function handle(Envelope $event): void { ProcessPayrollDisbursementBatch::dispatch($event->payload); }
Worker:
php artisan events:consume
Supervisor (next to queue:work):
php artisan events:consume
Directed messages (targets: ["app-b"]) are skipped by other groups. Broadcast (targets: ["*"]) is offered to every group; only registered listeners run.
Publish
use Hollyphat\EventBus\Facades\EventBus; // Way 1 — every backend sees it; each picks via $listen EventBus::broadcast('organization.user.synced', $payload); // Way 2 — only the app-b consumer handles it EventBus::send('app-b', 'payroll.disbursement.batch', $payload); // Command that needs a reply $result = EventBus::sendAndWait('app-c', 'seat.member.provision.requested', $payload, timeout: 45); // Inside the app-c handler EventBus::reply($event->correlationId, [ 'organization_user_id' => 12, 'account_user_id' => 34, ]);
Do not call sendAndWait inside the consume process that must answer that command (deadlock). The worker is a separate process.
Envelope
{
"specversion": "1.0",
"id": "ulid",
"event": "payroll.disbursement.batch",
"source": "app-c",
"targets": ["app-b"],
"occurred_at": "2026-09-14T14:00:00Z",
"idempotency_key": "payroll_disbursement_batch_123",
"correlation_id": null,
"payload": {}
}
targets: ["*"] = broadcast. Redis 6.2+ (XAUTOCLAIM). Failed handlers are not ACK’d so another consumer can claim them.
Tests
composer install
composer test
No live Redis required (stream client is faked).