Download the PHP package bear/event-sourcing without Composer

On this page you can find all versions of the php package bear/event-sourcing. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package event-sourcing

BEAR.EventSourcing

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 the recorded state changes: which write occurred, when, and to which resource.

Installation

Quick start

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

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

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.

What is an event

An Event is a successful resource operation observed at the root of a Semantic Logger log: a boundary request, an open/close pair of type resource_request with no parent. State-changing methods qualify by default; a root GET qualifies only under the opt-in WITH_READS policy below. It carries only the facts it observed:

Field Source
id derived from the observed facts (see below)
uri request uri
method request method (upper-cased)
params request params, falling back to query
timestamp request timestamp — absolute ISO-8601 with an explicit offset; an entry without one is not extracted
result close.context.body when present

Extraction is deterministic: it never invents facts. There is no fallback clock for a missing timestamp, and only root entries of type resource_request qualify — another subsystem's open/close pair that happens to carry method and uri fields is never misread as a state change, and a request nested inside another request is never extracted (see Recording and observation). Extracting the same log twice yields the same events.

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

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

A response code of 400 or greater marks a failed operation, which is ignored; a string of digits such as "404" is read the same way. A missing code is treated as success (no failure signal). A code that is present but cannot be read as a status — a non-digit string, a float, a boolean — is treated as a failure too, so only confirmed successes become events.

Event identity

Event::$id is a sha256 hash derived from method, uri, the timestamp normalized to UTC, and key-sorted params — the same operation observed at the same instant is the same event. result is excluded on purpose: the same domain operation produces the same event regardless of how its response body was recorded. So is replayable (see Redacting sensitive params): whether a filter withheld a credential is a property of how the request was recorded, not of the operation.

Identity is what makes the store a source of truth. Re-extraction reproduces the same ids, and every store treats append as idempotent per id, so a retried batch never duplicates facts.

Recording and observation

The log and the event stream answer different questions. The event stream records what can be reproduced: the boundary write requests (root reads only when opted in), the input a replay re-executes. The log observes what happened: every node — reads, failures, nested requests, durationMs — for transparency, debugging, and checking a replay against the original run.

Extraction therefore takes root entries only. A POST app://self/orders whose handler issues PUT app://self/inventory/SKU-1 yields one event, the POST. Replaying it re-executes the handler, which issues the PUT again; had the PUT been recorded as well, replay would apply it twice. This is the same boundary MySQL statement-based replication draws when it logs a statement but not the writes its triggers perform. The nested PUT is still in the log, as observation.

Replay by re-execution rests on two conditions the package assumes but does not enforce, the way MySQL flags unsafe statements rather than refusing them:

Two natural extensions are not implemented: verifying determinism by diffing the observation tree a replay produces against the original, and appending events inside the request's own transaction (an outbox). Runtime auto-persistence remains out of scope.

Redacting sensitive params (secure by default)

params is whatever the request carried — a login's password, a checkout's csrfToken — and it flows into both the log and, when extracted, Event::params and Event::$id. The resource bridge (SemanticLogInvoker) filters this by default: unbound, it constructs SensitiveParamsFilter, which replaces the value of every credential/transport-shaped key at every depth of params with [FILTERED] (a nested ['credentials' => ['password' => '…']] is walked the same way a flat one is, and an object value is walked on the JSON view the log will record; past SensitiveParamsFilter::MAX_DEPTH levels the walk stops and the rest of the subtree is replaced whole, keys included, so the per-key guarantee below holds down to that bound). The key stays — the same convention as Rails' filter_parameters and Sentry's scrubber — so the log still shows that a password was sent, just not what it was. An application opts out of this — never in — by binding its own #[Filtered] ParamsFilterInterface. To add this application's own credential-shaped key to the default set, construct the default with it — paramsFilter: new SensitiveParamsFilter(['otp']), or bind that instance with the qualifier — and it is matched like the built-in set and treated as a credential. Anything beyond that (narrowing the default for a pageToken, a different replayability verdict) is a filter of your own. Compose the default rather than replace it: a filter that only handles its own key silently switches the built-in protection off.

Bind it in the module that installs the observation wiring, or pass it as ResourceObservationModule(paramsFilter: …) / DevLogModule(paramsFilter: …) — one or the other, not both. Doing both is quiet rather than loud: the installing module's own #[Filtered] binding wins in either install order and the constructor argument is dropped without a word, because Ray.Di keeps the binding the installer already holds. Both recorders then agree on the binding, so the result is consistent, just not the one the discarded argument asked for. A binding made inside a module that ResourceObservationModule(module: $app) wraps loses the opposite way — the wrapping module's own bindings win, so the default overrides it.

Not every filtered key means the same thing, so the filter returns a FilteredParams — the params, and whether the operation is still replayable with them:

The default deliberately does not match a generic key suffix. An application's own identifiers just as often end in Key for reasons that have nothing to do with secrecy: an idempotencyKey is exactly the domain input a replay needs to stay deterministic, and filtering it by name pattern alone would make an otherwise-safe write silently non-replayable. apikey is the exception — nothing non-secret is named that way. A resetKey/authKey-shaped field an application actually wants redacted is a #[Filtered] ParamsFilterInterface it binds itself — this package does not know an arbitrary caller's naming conventions well enough to guess safely.

SemanticLogInvoker records replayable on the resource_request context — it does not itself decide whether a withheld value matters, only the filter does. A filter that throws never breaks the request, the way a body store that throws never breaks it: the request runs, nothing the filter failed on is recorded (params: [], replayable: false) and an E_USER_WARNING naming only the exception's class is raised. The parallel stops at the log, though — a body-store failure is recorded in the close context as exception: {class, message}, while a filter failure leaves no trace of its own in the log: the entry is still written with its uri, method and timestamp — only params is empty and replayable false — and the warning is the only place the cause is named. SemanticLogExtractor extracts a replayable: false request like any other, so the event stream stays a complete record of what happened, and the placeholder travels into Event::params (and Event::$id, which is therefore stable across re-extraction). The flag travels with it — Event::$replayable, excluded from the id, and a column in the SQL store — because the log is transient and the store is what a replay engine reads. An admin login is still an event — you can see it was attempted, by whom, when — and what a replay engine does with an event it cannot re-execute faithfully (skip it, seed the credential from elsewhere, stop) is that engine's call, not this package's:

Both become events; the second carries replayable: false.

This is a name-based guard, not a secret-value scanner: a credential shaped differently (a bare pin or otp, or a resetKey) is added through the constructor as above, and the transport/credential split is a default judgment call an application is free to override per key. Both of this package's recorders honour the same filter: request params here, and the bind values the Ray.MediaQuery adapter records (see Ray.MediaQuery observation). What it does not reach: the response body a BodyStoreInterface records as body_ref is written as-is, the message of an exception recorded in the close context is not filtered either, and an application's own loggers are a different boundary and need their own redaction.

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:

URI prefixes and timestamps work the same way:

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. Appending is idempotent per Event::$id in every implementation, so replaying or retrying a batch is always safe.

The SQL store and the BEAR.Resource bridge are optional features: ray/media-query and bear/resource are suggested dependencies, required only when you use them.

Use InMemoryEventStore for tests and development:

Use MediaQueryEventStore when the EventStore should be backed by SQL through Ray.MediaQuery. MediaQuery stays application-owned; the modules install flat, and EventSourcing modules never hide AuraSqlModule or MediaQuerySqlModule:

Forgetting MediaQuerySqlModule (or AuraSqlModule) surfaces as an explicit unbound error at injection time — never as a store that fails on first use.

Apply sql/event_store/schema.sql with your application's migration tool before using the SQL store; the bundled SQL uses SQLite dialect (INSERT OR IGNORE), so port the two statements when targeting another database. event_id is UNIQUE — that constraint is what makes appends idempotent. Timestamps are stored in UTC so the recorded_at index sorts in time order; replayable is stored as an integer flag. Because it is excluded from event_id, a re-append of an existing id is ignored whole and the stored flag is kept, like every other column: first write wins. A table created from the 0.1.0 schema lacks the column, and both append() and all() fail until it is added: ALTER TABLE event_store ADD COLUMN replayable INTEGER NOT NULL DEFAULT 1;. An application that copied sql/event_store/*.sql into its own sqlDir (below) must re-copy them too, and this is the migration step worth checking twice, because the two stale files fail differently. A stale event_store_list.sql does not select the column, and all() raises an EventStoreException naming the file — loud, and the reason the re-copy is hard to forget for long. A stale event_store_append.sql has no :replayable placeholder, and Ray.MediaQuery binds only the placeholders a file actually contains: the INSERT succeeds, the column takes its DEFAULT 1, and an event the filter marked non-replayable is stored as replayable with no error and no warning. Nothing inside this package can catch that one — INSERT OR IGNORE, which is what makes appends idempotent, swallows a constraint violation as readily as a duplicate id — so re-copy both files together. MediaQueryEventStore keeps JSON, timestamp and flag database mapping inside the adapter, not on Event.

A few operational notes:

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:

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.

The application owns the flush

The bridge only writes open/close entries; it never flushes or persists. Inject SemanticLoggerInterface, call $logger->flush() once per request, and extract (and optionally store) from the returned log. Two operational rules follow:

EventCollector packages flush -> extract -> optional append into one call for the handler that owns the request boundary. In worker runtimes, call it from the runtime's own request-end event (a Swoole request close, a RoadRunner worker-loop iteration) — no process-shutdown hook is involved:

The collector is the persistence boundary: it consumes the session and returns only the events — the durable facts. A development request end that also wants the tree owns the flush directly and extracts from the same log:

Note that the bundled ResourceResponseContext records code, body_ref, and durationMs — it never inlines the body. Events extracted from a bridge log therefore always carry a null result; the payload lives behind the body_ref pointer (see below). Record your own close context with a body field when the event itself must carry the result.

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

Wiring inside a BEAR.Sunday context

Passing module: is for a standalone injector, where the bridge's wrapped module is the only provider of InvokerInterface. A BEAR.Sunday context module (a dev- prefix such as dev-hal-app) inherits the whole inner chain instead, so it renames the chain's own binding and decorates it in place:

Do not re-provide the package bindings there — override(new DevLogModule(module: new PackageModule())) registers the framework pointcuts a second time and every interceptor runs twice. The cache log makes the double execution visible as a get scope nested in itself; without a log it is invisible.

Recording and extraction stay separate policies (#[Recorded] / #[Extracted]): the WITH_READS recording above does not widen the extractor's writes-only default.

A BodyStoreInterface records a body_ref in the close context:

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.

durationMs is the wall time of the invocation, children included. It is observation data: it stays in the close context and is never part of event identity, for the same reason result is excluded — the same domain operation is the same event regardless of how long it took. The log answers which request was slow and where in the tree; why remains the profiler's job.

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 DevLogModule is constructed, so it always reflects the latest run:

bodyDir must be a dedicated directory owned by the body store: it is cleared on each run, and to avoid deleting anything else it refuses to clear a directory it did not create or adopt while empty (an ownership marker guards this). Use one bodyDir per process; the sequence counter is per-store, so pointing concurrent processes at the same directory can overwrite each other's files. A failed render is recorded as an exception in the close context, not written as an empty body file.

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:

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, recorded as a leaf event with its intent and wall time (see the Ray.MediaQuery observation below). Every node follows one rule: the intent is inline, the heavy detail sits behind a *_ref pointer. 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.

Ray.MediaQuery observation (optional)

Ray.MediaQuery exposes a logger seam (MediaQueryLoggerInterface) that brackets each query execution. MediaQueryObservationModule routes it into the semantic log as one media_query leaf event per executed query — the query id, its converted parameters, and wall time measured in the adapter — nested under whichever scope is open. The parameters pass through the same #[Filtered] ParamsFilterInterface as request params (SensitiveParamsFilter when unbound), so a query that binds a TOTP secret or a reset token records [FILTERED]; the filter's replayability verdict is dropped, since a media_query entry is a leaf, not an event:

The module installs flat: only the logger binding — the adapter needs the unqualified SemanticLoggerInterface binding, the same instance the resource bridge and the flush owner use. It binds no #[Filtered] filter of its own: unbound, the adapter falls back to the default, and a binding here would collide with ResourceObservationModule's (whichever installed first would win for both recorders). Boundaries to know:

The log is a verifiable contract

Every context names its JSON Schema (schemaUrl). The canonical schema files live in docs/schemas/ and are published at https://bearsunday.github.io/BEAR.EventSourcing/schemas/. Validate a flushed log offline with Semantic Logger's validator:

examples/observe/observe.php ends by validating its own run, so a contract break fails the demo.

One tree with BEAR.QueryRepository (optional)

BEAR.QueryRepository's semantic cache log records through a #[CacheLog]-qualified SemanticLoggerInterface. Bind the same instance under both keys and the two observations become one tree: cache scopes nest inside the resource_request scope that caused them.

Share the instance with toInstance(): two separate to(...)->in(Scope::SINGLETON) bindings would create one singleton per binding key and split the tree in two. Extraction stays safe in the merged tree — only resource_request entries become events, so cache scopes are never misread as state changes. tests/UnifiedLogTest.php proves both properties.

Under a compiled injector (a BEAR.Sunday context), alias with a provider instead of toInstance — the compiler would serialize the logger instance and each process would get its own copy, splitting the tree again:

Agent skill: bear-observe

skills/bear-observe/ is a Claude Code skill that writes the wiring above into an application, proves it from the bindings, renders one request as a tree, and reads that tree: the cache event vocabulary, what each #[Cacheable]-family declaration is supposed to look like in the log, and app-vs-library triage. Install it into a project:

Then ask an agent to read the log. It runs harness/setup.php only when observation is not already working, so an application that is already wired is left alone.

Its DevModule template aliases in the opposite direction from the snippet above: DevQueryRepositoryLogModule already binds the #[CacheLog] key, so the template points the plain SemanticLoggerInterface at that one. Either direction gives one tree; this one does not restate the writer and the shutdown sink.

Boundaries


All versions of event-sourcing with dependencies

PHP Build Version
Package Version
Requires php Version ^8.3
koriym/semantic-logger Version ^0.9
ray/di Version ^2.19
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package bear/event-sourcing contains the following files

Loading the files please wait ...