Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Eventstore Client Laravel Package

rxnet/eventstore-client

RxNet EventStore Client integrates EventStoreDB with Laravel, making event sourcing practical in PHP. Append and read events, work with streams, and build event-driven applications through a simple, framework-friendly client API.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the client and its async dependencies. The package is pure PHP, so it drops into a Laravel app without a native extension:

composer require rxnet/eventstore-client

You also need a running EventStoreDB server. This client speaks the legacy TCP protocol (tcp://host:1113), so you need EventStore 3.x/4.x/5.x — not the gRPC-only 20.x+ servers. For a local trial, run the old Docker image with the TCP port mapped:

docker run --rm -p 1113:1113 -p 2113:2113 eventstore/eventstore:5.0.9

Where to look first:

  • Rxnet\EventStore\EventStore — the entry point (connect, read, write, subscribe, delete, transactions).
  • Rxnet\EventStore\Data\* — DTOs you actually construct: NewEventData, EventData, EventRecord, WriteResult, ReadResult.
  • Rxnet\EventStore\Acknowledgeable — the wrapper returned from writes, exposing ack/nack observables.

A minimal first use case — connect, append one event, read it back:

use React\EventLoop\Loop;
use Rxnet\EventStore\EventStore;
use Rxnet\EventStore\Data\NewEventData;
use Rxnet\EventStore\Data\EventRecord;

$es = new EventStore('tcp://127.0.0.1:1113', Loop::get());

$es->connect()->subscribe(
    fn() => print "connected\n",
    fn(\Throwable $e) => print "connect error: {$e->getMessage()}\n"
);

$event = new NewEventData();
$event->setEventId(/* uuid */);
$event->setEventType('order.placed');
$event->setData(json_encode(['orderId' => 42]));
$event->setMetadata(json_encode(['user' => 'system']));

$es->write('order-42', [$event])->subscribe(
    fn($write) => print "written\n",
    fn(\Throwable $e) => print "write error: {$e->getMessage()}\n",
    fn() => $es->read('order-42', 10, 0, true, false)->subscribe(
        fn(EventRecord $r) => print "read: {$r->getEventType()}\n"
    )
);

Loop::run(); // the loop drives everything; without it nothing happens

The takeaway: nothing executes until the ReactPHP loop runs, and everything is an observable you must subscribe() to.

Implementation Patterns

Register the client as a singleton

Bind one EventStore + one loop per process rather than per request. Put it behind a service provider and inject it:

$this->app->singleton(EventStore::class, function () {
    return new EventStore(config('eventstore.dsn'), Loop::get());
});

config/eventstore.php should hold dsn, default_stream, credentials, and timeouts — keep them out of code.

Writes are fire-and-forget until you ack

write() returns an Acknowledgeable. Subscribing yields a WriteResult and an ack/nack channel. In application code, always attach both handlers so a broker-level reject doesn't get silently dropped:

$es->write($stream, [$newEvent])->subscribe(
    function ($acknowledgeable) use ($log) {
        $acknowledgeable->ack()->subscribe(
            fn() => $log->info('event committed'),
            fn($e) => $log->error('ack failed', ['e' => $e])
        );
    },
    fn($e) => $log->error('write rejected', ['e' => $e])
);

Use write + subscribe for read models, not a full event bus

The package is a client, not a message bus. The idiomatic pattern is: a Laravel job or command appends domain events to a per-aggregate stream (order-42), and a separate projection reads with read() or readAll() from a checkpoint. Because read() is paged, wrap it in a recursive Rx chain or a while loop that requests the next slice until you reach the end of the stream.

Live subscriptions for projections

$es->subscribe($stream, $resolveLinkTos) gives you a live feed. Drive projections from that observable, keeping the last-seen eventNumber in a checkpoints table so you can resume. Subscribe from your checkpoint on restart — the package itself does not remember position.

Leverage Rx operators for pipeline logic

The reactive flavour is the actual value of this package. You get .map(), .filter(), .flatMap() and friends over event streams, so a projection becomes:

$es->read('order-42', 1000, 0, true, false)
   ->filter(fn($e) => $e->getEventType() === 'order.placed')
   ->map(fn($e) => json_decode($e->getData(), true))
   ->subscribe(fn($payload) => $this->applyPlaced($payload));

That's cleaner than a foreach for aggregating/streaming logic — lean on it.

Isolate the loop in CLI, not HTTP

Run the client inside php artisan commands, queue workers, or a dedicated projection process. For request-scoped operations, either:

  • push the work to a queued job (preferred — the job runs the loop and finishes), or
  • block explicitly with the client's synchronous bridge / ->wait() on the observable (acceptable for small reads/writes, but you pay a loop-startup cost per request).

Integration tips

  • Transactions: EventStore exposes transaction helpers, but for the 90% case write() with expectedVersion (via the stream options) is enough. Use explicit expected versions to get optimistic concurrency — the server will reject on mismatch and your nack handler should retry or surface a domain conflict.
  • Domain events: wrap NewEventData in a small factory that maps your PHP value objects to event type + JSON payload + metadata (including correlationId, causationId, user). This is where you keep your Laravel app idiomatic.
  • Testing: unit-test your factories and projection appliers directly. For integration tests, fake the EventStore behind your own interface (an in-memory append/read) rather than mocking Rx graphs — observables are painful to stub.

Gotchas and Tips

  • The package is stale (2019). It has not tracked EventStoreDB's shift to gRPC. Modern EventStoreDB (20.x+ with gRPC-only, 21.x+ dropping TCP) will not work with this client. Budget for pinning an old server or moving to a maintained gRPC client. This is the biggest single risk.
  • Modern PHP/ReactPHP drift. It assumes older react/event-loop and rx/observable APIs. On PHP 8.1+ and current ReactPHP you may hit signature/type issues. Pin versions or be prepared to shim.
  • Nothing happens without the loop. Forgetting Loop::run() (or ->wait()) yields a silent no-op — writes appear "lost," reads return nothing, and no exception is raised because the promise never settles. Symptom: process exits cleanly with empty output.
  • Errors are swallowed inside observables. A failed subscribe() without an onError handler disappears. Always pass both error and completion callbacks, especially on connect() and write(). Add a global loop-level error logger.
  • No automatic reconnection. If the TCP connection drops, existing subscriptions die and the client will not retry. Wrap connect()/subscribe() in a supervisor with exponential backoff and resume from your own checkpoint.
  • resolveLinkTos matters. If you emit $> links, reading without resolveLinkTos = true returns link records, not events — the most common cause of "my event type is $>" confusion.
  • Backpressure. Observable chains over a hot subscription can outpace your projection applier. Don't do synchronous DB writes inside onNext on a live feed without batching/queueing; buffer to a queue and drain.
  • expectedVersion is your concurrency control. Use it. Silent last-write-wins is the default otherwise and will corrupt aggregate histories.
  • Octane / long-running workers. A shared EventStore bound as a singleton plus a shared loop is not safe across Octane requests — the loop lifecycle and connection state get confused. Keep event-store access in dedicated CLI/queue workers, or scope a fresh client per request.
  • Extension point: the loop. The EventStore constructor takes a loop. Inject a loop you control (e.g. from a supervisor command) so you can add signals, timeouts, and health checks around it instead of letting the client own it.
  • DTOs use setters, not constructors with args. NewEventData requires setEventId, setEventType, setData before write; missing fields fail late, at protocol level, not at object construction. Validate in your factory.
  • No PSR-3 / no framework integration. Bring your own logging wrapper and service provider — there is no out-of-the-box Laravel package here.

Overall: an elegant reactive client that showcases RxPHP nicely, but it targets a protocol generation EventStoreDB has largely left behind. Use it if you already operate an old EventStore cluster or are learning event sourcing; for greenfield work, treat it as a reference rather than a foundation.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route