Search by

hollyphat / event-bus

hollyphat

Shared Redis Streams event bus for distributed Laravel applications.

0.1.0 2026-09-17 17:45 UTC

This package is auto-updated.

Last update: 2026-09-17 18:17:19 UTC


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).