定制 bear/event-sourcing 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

bear/event-sourcing

Composer 安装命令:

composer require bear/event-sourcing

包简介

Event sourcing primitives extracted from Semantic Logger observations

README 文档

README

Make your application's state changes a replayable source of truth — fold the events back to reconstruct state at any point in time.

Every event is a resource operation: a method on a uri, like POST app://self/users. That resource shape keeps replay straightforward, and it lets the same event stream double as an audit history of what happened, when, and to which resource.

Installation

composer require bear/event-sourcing

Quick start

Flush a Semantic Logger log, extract events, and iterate them:

use BEAR\EventSourcing\SemanticLogExtractor;

$log = $semanticLogger->flush(); // Koriym\SemanticLogger\LogJson
$events = (new SemanticLogExtractor())->extract($log);

foreach ($events as $event) {
    // $event->uri, $event->method, $event->params, $event->result, $event->timestamp
    echo $event->method, ' ', $event->uri, "\n";
}

SemanticLogExtractor implements SemanticLogExtractorInterface, so it can be injected. Install EventSourcingModule and pass RecordedMethods only when the extraction policy differs from the default:

use BEAR\EventSourcing\Module\EventSourcingModule;
use BEAR\EventSourcing\RecordedMethods;
use Ray\Di\AbstractModule;

final class AppModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->install(new EventSourcingModule(
            methods: new RecordedMethods(RecordedMethods::WITH_READS),
        ));
    }
}

How it works

Semantic Logger is the observation source. The package reads a flushed Semantic Logger log and derives immutable events from its public open/close tree — it adds no event-dispatch code to your domain and persists nothing on its own.

Semantic Logger observations -> Events -> optional EventStore

What is an event

An Event is a successful, state-changing, resource-like operation observed in a Semantic Logger open/close pair. It carries only the facts it observed:

Field Source
uri request uri
method request method (upper-cased)
params request params, falling back to query
timestamp request timestamp, falling back to extraction time
result close.context.body when present

Recorded methods by default — GET is observation data, not a state change, so it is ignored:

  • POST
  • PUT
  • PATCH
  • DELETE

Include GET explicitly for development-time read tracing by injecting new RecordedMethods(RecordedMethods::WITH_READS).

If close.context.code exists and is 400 or greater, the operation is treated as unsuccessful and ignored.

Filtering and replay

Events is a countable, iterable collection. Keep it small and select with PHP's standard iterators instead of adding query methods — filters stack without changing the collection:

use BEAR\EventSourcing\Event;

$userEvents = new CallbackFilterIterator(
    $events->getIterator(),
    static fn (Event $event): bool => ($event->params['id'] ?? null) === 'koriym',
);

$userWrites = new CallbackFilterIterator(
    $userEvents,
    static fn (Event $event): bool => $event->method !== 'GET',
);

foreach ($userWrites as $event) {
    // replay, project, or inspect events for id=koriym
}

URI prefixes and timestamps work the same way:

$orderEvents = new CallbackFilterIterator(
    $events->getIterator(),
    static fn (Event $event): bool => str_starts_with($event->uri, 'app://self/orders/123'),
);

See examples/extract.php, examples/replay.php, and examples/store.php for runnable end-to-end scripts.

Storage (optional)

Persist extracted events explicitly when an application needs storage. EventStoreInterface is a small persistence port (append, appendAll, all), not a runtime hook.

Use InMemoryEventStore for tests and development:

use BEAR\EventSourcing\Store\InMemoryEventStore;

$store = new InMemoryEventStore();
$store->appendAll($events);

Use MediaQueryEventStore when the EventStore should be backed by SQL through Ray.MediaQuery. MediaQuery stays application-owned; EventSourcingModule only adds the EventStore binding and never hides AuraSqlModule or MediaQuerySqlModule:

use BEAR\EventSourcing\EventStoreInterface;
use BEAR\EventSourcing\Module\EventSourcingModule;
use BEAR\EventSourcing\Module\MediaQueryEventStoreModule;
use Ray\AuraSqlModule\AuraSqlModule;
use Ray\Di\AbstractModule;
use Ray\Di\Injector;
use Ray\MediaQuery\MediaQuerySqlModule;

final class AppModule extends AbstractModule
{
    protected function configure(): void
    {
        $packageDir = __DIR__ . '/vendor/bear/event-sourcing';
        $this->install(new AuraSqlModule('sqlite:' . __DIR__ . '/events.sqlite'));
        $this->install(new MediaQuerySqlModule(
            interfaceDir: $packageDir . '/src/Query',
            sqlDir: $packageDir . '/sql/event_store',
        ));
        $this->install(new EventSourcingModule(
            store: new MediaQueryEventStoreModule(),
        ));
    }
}

$store = (new Injector(new AppModule()))->getInstance(EventStoreInterface::class);
$store->appendAll($events);

Apply sql/event_store/schema.sql with your application's migration tool before using the SQL store. MediaQueryEventStore keeps JSON and timestamp database mapping inside the adapter, not on Event.

BEAR.Resource observation bridge (optional)

To produce Semantic Logger open/close entries from BEAR.Resource execution, decorate InvokerInterface — not LoggerInterface. ResourceObservationModule wraps an existing BEAR.Resource module:

use BEAR\EventSourcing\Resource\ResourceObservationModule;
use BEAR\Resource\Module\ResourceClientModule;
use Ray\Di\Injector;

$injector = new Injector(new ResourceObservationModule(
    module: new ResourceClientModule(),
));

By default the bridge installs NullBodyStore, so no body is stored. Applications that need payload inspection can provide their own BodyStoreInterface and store the body in files, SQL, object storage, or test fixtures.

For local AI/debug work, use DevLogModule. It clears the body directory when the injector is created, stores rendered bodies as files through FileBodyStore, and records GET as well as write methods:

use BEAR\EventSourcing\Resource\DevLogModule;
use BEAR\Resource\Module\ResourceClientModule;
use Ray\Di\Injector;

$injector = new Injector(new DevLogModule(
    bodyDir: __DIR__ . '/var/es/bodies',
    module: new ResourceClientModule(),
));

A BodyStoreInterface records a body_ref in the close context:

{"code": 200, "body_ref": "file:///path/to/var/es/bodies/000001.json"}

body_ref is a reference to a stored rendered body. It stays in the Semantic Log for inspection and is not extracted into Event::$result — the event's result comes from close.context.body. A bridge log that records only body_ref therefore yields an event with a null result; the payload lives in the externalized body, not in the event. The same domain operation produces the same event regardless of which BodyStoreInterface the bridge uses.

What dev observation produces

With DevLogModule active you read two artifacts:

Body files under bodyDir, one per recorded operation, numbered in invocation order. The directory is cleared when the injector is created, so it always reflects the latest run:

var/es/bodies/000001.json   # rendered body of the first recorded operation, i.e. (string) $ro
var/es/bodies/000002.json

The Semantic Logger log, a nested open/close tree held in memory until you call $logger->flush(). Render it as a readable tree — far smaller than the raw JSON, for both humans and AI. Resource\Stree\ResourceNodeFormatter renders each node as one resource operation, so a POST app://self/orders that internally calls PUT app://self/inventory/SKU-1 reads as intent in, result out:

request="POST app://self/orders?order_id=O-1000"
├── request="PUT app://self/inventory/SKU-1?sku=SKU-1&quantity=1"
│   ├── media_query name=inventory_reserve sku=SKU-1
│   │   └── rows_ref=file://var/es/rows/000001.json
│   └── code=200 body_ref=file://var/es/bodies/000001.json
└── code=201 body_ref=file://var/es/bodies/000002.json

The request line is the intent (method on a uri with its params as a query string); the └── close line is the result (code plus the body_ref pointer). Child operations nest under their parent — a resource calling a resource, and a resource embedding a non-resource operation such as a media query, which renders in stree's generic form but stays structurally clear. Every node follows one rule: the intent is inline, the heavy detail sits behind a *_ref pointer (body_ref, rows_ref). The resource shape keeps the tree normalized, and no timestamp noise leaks in. When debugging, follow a node's *_ref to its file for the full detail.

Render it with TreeRenderer and a FormatterRegistry that registers ResourceNodeFormatter for the resource_request type — examples/tree.php builds a DevLogModule-style log (body_ref pointers) and renders it, and examples/semantic-tree.txt is its output. The bundled vendor/bin/stree dev-log.json works too, but renders the generic form (type label plus a raw timestamp) since the CLI does not load custom formatters. This package never writes the log to disk itself; examples/semantic-log.json is the raw LogJson of the extraction examples.

Boundaries

  • Semantic Logger is the observation source; EventStore is an optional destination.
  • No automatic persistence during runtime observation.
  • No BEAR\Resource\LoggerInterface decorator.
  • Ray.MediaQuery and database installation stay in the application, never hidden inside EventSourcing modules.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 3
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-27