Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
944e504
feat(eventsourcing): add the firefly/eventsourcing package and regist…
Oct 13, 2026
d6727f2
feat(eventsourcing): read firefly.eventsourcing.* once and refuse wha…
Oct 13, 2026
e9bf70e
feat(eventsourcing): define the EventStore port, the event envelope w…
Oct 13, 2026
b87a7e0
feat(eventsourcing): upcast stored events through a type-and-version …
Oct 13, 2026
42e477b
feat(eventsourcing): add the memory event store and the contract ever…
Oct 13, 2026
8804e0a
fix(eventsourcing): match the readAll filter against the stored event…
Oct 13, 2026
011f742
feat(eventsourcing): add the database event store, numbered in commit…
Oct 13, 2026
c749a9b
fix(eventsourcing): take the SQLite write lock before the head check …
Oct 13, 2026
4ddb008
test(eventsourcing): prove the event store, racing appenders and late…
Oct 13, 2026
fa99716
feat(eventsourcing): compile #[EventType] events into a registry and …
Oct 13, 2026
e130046
fix(eventsourcing): refuse a convention-mapped event whose constructo…
Oct 13, 2026
dc4784a
fix(eventsourcing): read a pure enum back by its case name and refuse…
Oct 13, 2026
c5b1270
fix(eventsourcing): refuse at scan time a convention-mapped union wit…
Oct 13, 2026
d06b954
feat(cli): compile the event-sourcing manifest in firefly:cache and r…
Oct 13, 2026
5bfa852
feat(eventsourcing): add the event-sourced aggregate base with conven…
Oct 13, 2026
19bdc45
feat(eventsourcing): add memory and database snapshot stores with kee…
Oct 13, 2026
559f20a
feat(eventsourcing): add the snapshot-aware repository, metadata cont…
Oct 13, 2026
15b535d
feat(eventsourcing): publish appended events through the outbox in th…
Oct 13, 2026
e72663d
feat(eventsourcing): auto-configure the stores, the repository suppor…
Oct 13, 2026
4677728
test(eventsourcing): prove same-transaction outbox publishing and aft…
Oct 13, 2026
c94512b
feat(eventsourcing): add the projection port, function projections an…
Oct 13, 2026
269932c
fix(eventsourcing): fence a late checkpoint advance on MariaDB snapsh…
Oct 13, 2026
f3d00fc
feat(eventsourcing): run projections under a DistributedLock lease wi…
Oct 13, 2026
d26f383
feat(eventsourcing): auto-configure the projection runner, report pro…
Oct 13, 2026
e796468
feat(eventsourcing): model an orchestration execution as an event-sou…
Oct 13, 2026
f927164
feat(eventsourcing): add the event-sourced orchestration execution st…
Oct 13, 2026
ade779e
feat(eventsourcing): supply firefly.orchestration.persistence.provide…
Oct 13, 2026
9827e1f
test(eventsourcing): prove event-sourced orchestration runs across te…
Oct 13, 2026
3865e76
docs(eventsourcing): document event sourcing, its database support an…
Oct 13, 2026
5ef593c
docs(eventsourcing): state the at-most-once guarantee of after-commit…
Oct 13, 2026
a5d9e76
docs(eventsourcing): stop calling event sourcing future work in the R…
Oct 13, 2026
43ee4fd
docs(cqrs): link the shipped event-sourcing projections instead of a …
Oct 13, 2026
8266f98
docs(cqrs): point the package README at the shipped eventsourcing pro…
Oct 13, 2026
dff83a2
Merge remote-tracking branch 'origin/main' into feat/eventsourcing
Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ All notable changes to LaraFly are documented here. This project uses CalVer (`Y

### Added

- **Event sourcing (`firefly/eventsourcing`).** An `EventStore` port on `memory` or portable `database` tables
(`firefly.eventsourcing.store.provider`): appends under optimistic concurrency on
`UNIQUE(aggregate_id, aggregate_version)` (a 409 `ConcurrencyException` for the losing writer), stream loads by
version range, and one global stream numbered in COMMIT order, so a reader never skips an event that committed
late, proven with racing appenders on PostgreSQL, MySQL, MariaDB, SQL Server and SQLite. Event envelopes carry
correlation, causation, tenant and user metadata; `#[EventType(name, version)]` events are compiled by
`firefly:cache`. Event-sourced aggregates and a snapshot-aware `EventSourcedRepository`; snapshots every
`snapshot.threshold` events with keep-count retention; `EventUpcaster` chains applied on read; projections with
fenced `memory`/`database` checkpoints, a per-batch `DistributedLock` lease, catch-up, live mode and rebuild,
`php artisan firefly:eventsourcing:projections{,:run,:rebuild}` and an `eventsourcing` health component with
projection lag; same-transaction publishing through the eda-database outbox (`publish.enabled`), otherwise after
commit, at most once; opt-in tenancy (`tenancy.enabled`).
- **`firefly.orchestration.persistence.provider=event-sourced`.** Executions as event-sourced aggregates with
snapshots and an inline, rebuildable read model on the event store's connection
(`firefly:eventsourcing:projections:rebuild orchestration.executions`), passing the orchestration persistence
contract on every server.
- **CQRS query cache (`firefly.cqrs.query.cache.provider=cache`).** `CacheQueryCache` on any Laravel cache store:
`#[CachedQuery(ttl:, group:)]` on a query class caches its results under a key derived from the query value
object; `#[InvalidatesQuery]` on an event class evicts one entry or a whole group when the event commits in
Expand Down Expand Up @@ -109,6 +125,11 @@ All notable changes to LaraFly are documented here. This project uses CalVer (`Y
`provider=postgres` reads the `firefly.eda.database.*` keys, with every `firefly.eda.postgres.*` key kept as an
alias. The outbox table gains `claimed_by` / `claimed_until` (run `php artisan migrate`).
- **Transaction timeouts on SQL Server** set the session's `LOCK_TIMEOUT` and restore it afterwards.
- **The CQRS module doc points at the shipped projections.** `docs/modules/cqrs.md` no longer calls read models
a future package; it links the `firefly/eventsourcing` projections section.
- **The cqrs package README points at the shipped projections.** `packages/cqrs/README.md` no longer calls read
models a future package; it links the `firefly/eventsourcing` projections section, and the event-sourcing prose
guard now reads package READMEs too.

### Fixed

Expand All @@ -122,6 +143,8 @@ All notable changes to LaraFly are documented here. This project uses CalVer (`Y
- **`RabbitMqRoundTripTest` failed against a real broker when `FIREFLY_RABBITMQ_DSN` was a URL**, which the old
parser read as one key and silently replaced with `127.0.0.1:5672`.
- **The lumen sample's Postgres round trip called `firefly:eda:consume` without registering it.**
- **README's roadmap still called event sourcing future work.** The bullet and `docs/modules/cqrs.md`'s
known-latent list now point at `firefly/eventsourcing`; a docs test refuses the claim while the package ships.
- **The CQRS/EDA bridge figure still credited only the Postgres adapter with same-transaction atomicity.** Its
caption now names the database outbox (`provider=database` or `postgres`), and its adapter row draws the
`database` provider; a test derives both providers from their outbox auto-configurations.
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1080,7 +1080,7 @@ Full flag reference and generated-file contents: [CLI](docs/cli.md).

One published library, `fireflyframework/larafly`, contains the components under `packages/*` and the bundled
application skeleton. Its `replace` entries satisfy component requirements at the framework's version;
component names remain useful module boundaries, with their own test suites. The 38
component names remain useful module boundaries, with their own test suites. The 39
[module guides](docs/modules/) below group them by concern:

| Group | Module | Package(s) |
Expand Down Expand Up @@ -1108,6 +1108,7 @@ component names remain useful module boundaries, with their own test suites. The
| Eventing & Messaging | [Webhooks](docs/modules/webhooks.md) — inbound webhooks: an endpoint per provider, HMAC/Stripe/GitHub/Twilio signatures, a replay window, idempotency, published to EDA | `firefly/webhooks` |
| Eventing & Messaging | [Callbacks](docs/modules/callbacks.md) — outbound callbacks: subscriptions, signed deliveries driven by events, retries, a circuit breaker per endpoint, a replayable execution log | `firefly/callbacks` |
| Eventing & Messaging | [Orchestration](docs/modules/orchestration.md) — sagas, TCC, durable workflows, recovery, DLQ | `firefly/orchestration` |
| Eventing & Messaging | [Event Sourcing](docs/modules/eventsourcing.md) — event store, aggregates, snapshots, upcasters, projections, outbox publishing | `firefly/eventsourcing` |
| CQRS | [CQRS](docs/modules/cqrs.md) — `CommandBus`/`QueryBus`, the domain→integration-event bridge | `firefly/cqrs` |
| Security | [Security](docs/modules/security.md) — principal model, `HttpSecurity`, `#[PreAuthorize]`, JWT/OAuth2 | `firefly/security` |
| Security | [OAuth2 Client](docs/modules/security-oauth2-client.md) — OpenID Connect login with presets and discovery, PKCE, id-token validation, `OidcUser`, RP-initiated logout, client credentials, `Http::oauth2Client()` | `firefly/security-oauth2-client` |
Expand Down Expand Up @@ -1174,9 +1175,11 @@ accurately:
credentials, refresh rotation, introspection, revocation, userinfo and RP-initiated logout). IdP adapters
beyond those presets (Cognito, internal-db) and MFA are deferred to their own future packages, matching the
Java Firefly Framework's module topology.
- **Read models / projections as a first-class concept.** The sample's `LedgerProjector` shows the pattern
today via a plain `#[EventListener]`; a dedicated `firefly/eventsourcing`-style package for event
sourcing/snapshots/projections is future work, as it is in PyFly.
- **Event sourcing in the tutorial and the book.** Event stores, aggregates, snapshots, upcasters and
checkpointed, rebuildable projections ship in [`firefly/eventsourcing`](docs/modules/eventsourcing.md),
shaped after the Java library and PyFly's `pyfly.eventsourcing`. What remains is the walkthrough: the
sample's `LedgerProjector` still builds its read model from a plain `#[EventListener]`, and neither the
tutorial nor the book has a chapter on the package.
- **Documentation.** The end-to-end [tutorial](docs/tutorial.md) (EN + ES, through a database outbox and a saga), the *larafly by example*
[book](book/README.md) (18 chapters + appendices, EN + ES, PDF + EPUB), and a
[docs table of contents](docs/README.md) all shipped with the documentation-parity milestone.
Expand Down
2 changes: 1 addition & 1 deletion book/src-es/02-dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ php artisan firefly:cache
```

```
firefly:cache — wrote 15 manifest(s) + 0 proxy(ies) to /path/to/my-app/bootstrap/cache/firefly
firefly:cache — wrote 16 manifest(s) + 0 proxy(ies) to /path/to/my-app/bootstrap/cache/firefly
```

Ya ejecutaste esto una vez, de forma indirecta — `firefly new` lo llama por ti en su script `post-create-project-cmd`, que es la razón por la que la aplicación del Inicio rápido arrancó correctamente sin que ejecutaras el comando a mano ni una sola vez. De aquí en adelante, cada vez que añadas o cambies un `#[Service]`, `#[Repository]`, `#[Configuration]` o cualquier otro atributo de Firefly, vuelve a ejecutar `firefly:cache` para recompilar el manifiesto; `php artisan firefly:clear` elimina la caché compilada y vuelve a la ruta de escaneo de desarrollo (más lenta, basada en reflexión).
Expand Down
2 changes: 1 addition & 1 deletion book/src-es/03-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ foreach ($this->classesIn($prefix, $dir) as $class) {
}
```

`ConfigManifestCompiler` escribe esa lista en `bootstrap/cache/firefly/config-properties.php` con la misma técnica basada en `var_export()`, sin reflexión, que usó el manifiesto de componentes. `php artisan firefly:cache` ejecuta este escaneo como uno de los trece pares escáner/compilador que orquesta — nunca invocas `ConfigPropertiesScanner` tú mismo.
`ConfigManifestCompiler` escribe esa lista en `bootstrap/cache/firefly/config-properties.php` con la misma técnica basada en `var_export()`, sin reflexión, que usó el manifiesto de componentes. `php artisan firefly:cache` ejecuta este escaneo como uno de los catorce pares escáner/compilador que orquesta — nunca invocas `ConfigPropertiesScanner` tú mismo.

En el arranque, `Firefly\Cli\Boot\FireflyCacheServiceProvider` carga ese manifiesto compilado y se lo entrega directamente a `Firefly\Config\Registrar\ConfigRegistrar::register()`, que vincula el DTO de cada descriptor como singleton del contenedor en un solo paso:

Expand Down
6 changes: 3 additions & 3 deletions book/src-es/07-cqrs.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,11 +555,11 @@ Los grupos se expulsan por **generaciones**, no por etiquetas de caché, así qu

`HandlerScanner` es el **único** punto de reflexión en `packages/cqrs/src` (la misma invariante forzada por grep que el Capítulo 6 describió para `firefly/domain`). Recorre una vez las raíces PSR-4 de la aplicación, resuelve cada `#[CommandHandler]`/`#[QueryHandler]` (ejecutando exactamente la inferencia de `messageType()` que acabas de leer) en un `HandlerDescriptor`, y entrega la lista completa a `HandlerManifestCompiler`, que la `var_export()`a en un archivo PHP de array plano — la misma técnica sin reflexión que el Capítulo 2 introdujo para el manifiesto de componentes y el Capítulo 3 para `#[ConfigProperties]`.

Nunca ejecutas `HandlerScanner` tú mismo. Es uno de los **trece** pares escáner/compilador que `php artisan firefly:cache` orquesta en un solo paso, junto al de componentes/contexto, rutas, propiedades de configuración y (del Capítulo 9) el transaccional:
Nunca ejecutas `HandlerScanner` tú mismo. Es uno de los **catorce** pares escáner/compilador que `php artisan firefly:cache` orquesta en un solo paso, junto al de componentes/contexto, rutas, propiedades de configuración y (del Capítulo 9) el transaccional:

```bash
$ php artisan firefly:cache
firefly:cache — wrote 15 manifest(s) + 1 proxy(ies) to /path/to/my-app/bootstrap/cache/firefly
firefly:cache — wrote 16 manifest(s) + 1 proxy(ies) to /path/to/my-app/bootstrap/cache/firefly
```

El archivo compilado aterriza en `bootstrap/cache/firefly/handlers.php`; `FireflyCacheServiceProvider` lo vincula directamente sobre la entrada del contenedor `HandlerManifest` — sobrescribiendo incondicionalmente el `HandlerManifest` **vacío** por defecto que `CqrsWiringProvider` vincula cuando todavía no existe ninguna caché (el mismo patrón de retirada al estilo `#[ConditionalOnMissingBean]` que el Capítulo 2 te mostró para los beans, aplicado aquí a todo un manifiesto compilado). `CqrsHandlerWiringPass` recorre entonces ese manifiesto en el arranque y llena el `HandlerRegistry` que conociste arriba con un invocador por descriptor — cada invocador resuelve su bean manejador **de nuevo desde el contenedor en cada despacho**, nunca en caché en tiempo de cableado, así que un manejador `#[Transactional]` como `OpenWalletHandler` siempre se observa a través de su proxy generado (Capítulo 9), nunca a través de la clase desnuda, sin proxy.
Expand All @@ -579,7 +579,7 @@ Equivócate en esto en una aplicación — añade un manejador, olvida volver a
| `#[CachedQuery]` / `#[InvalidatesQuery]` | Guarda la respuesta de una consulta por valor en un almacén de caché de Laravel (`firefly.cqrs.query.cache.provider=cache`); el evento que la deja obsoleta expulsa una entrada (`with:`) o un grupo tras confirmarse |
| `CommandProcessingException` / `QueryProcessingException` | Envoltorios que preservan la categoría; el fallo original siempre es alcanzable vía `getPrevious()` |
| `HandlerRegistry` | Una clase de comando/consulta se mapea a exactamente un manejador — un registro duplicado falla ruidosamente en tiempo de cableado |
| `HandlerScanner` → `HandlerManifest` | El único punto de reflexión; compilado por `firefly:cache` (uno de trece pares escáner/compilador) en un manifiesto sin reflexión |
| `HandlerScanner` → `HandlerManifest` | El único punto de reflexión; compilado por `firefly:cache` (uno de catorce pares escáner/compilador) en un manifiesto sin reflexión |

---

Expand Down
2 changes: 1 addition & 1 deletion book/src-es/08-eda-outbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ final class OrderService

Aquí el destino (`'orders'`) y el tipo de evento (`'order.placed'`) son cadenas deliberadamente distintas, y el patrón `'order.*'` coincide con la **segunda** — precisamente para que este ejemplo no pueda confundirse con evidencia de que los patrones alguna vez miran el destino.

`EventListenerScanner` (`packages/eda/src/Scanner/EventListenerScanner.php`) es el único sitio de reflexión en `firefly/eda`, recorre una vez las raíces PSR-4 de la aplicación y compila cada método `#[EventListener]` en un `EventListenerDescriptor`; `EventListenerManifest` carga el resultado sin reflexión alguna, exactamente el mismo idioma de `HandlerManifest` que te mostró el Capítulo 7. `firefly:cache` ejecuta este escáner como uno de sus trece pares, escribiendo `bootstrap/cache/firefly/event-listeners.php`; `FireflyCacheServiceProvider` lo vincula, sustituyendo el valor por defecto vacío con el que arrancaría una aplicación sin caché. `EventListenerWiringPass` recorre después ese manifiesto al arrancar — en *todo* proceso, tanto en la petición web como en el worker de cola — envuelve cada invocación de destino en el decorador de reintento/DLQ de más abajo, y llama a `$bus->subscribe($pattern, $wrapped)` por cada patrón, resolviendo el bean de destino **fresco desde el contenedor en cada despacho**.
`EventListenerScanner` (`packages/eda/src/Scanner/EventListenerScanner.php`) es el único sitio de reflexión en `firefly/eda`, recorre una vez las raíces PSR-4 de la aplicación y compila cada método `#[EventListener]` en un `EventListenerDescriptor`; `EventListenerManifest` carga el resultado sin reflexión alguna, exactamente el mismo idioma de `HandlerManifest` que te mostró el Capítulo 7. `firefly:cache` ejecuta este escáner como uno de sus catorce pares, escribiendo `bootstrap/cache/firefly/event-listeners.php`; `FireflyCacheServiceProvider` lo vincula, sustituyendo el valor por defecto vacío con el que arrancaría una aplicación sin caché. `EventListenerWiringPass` recorre después ese manifiesto al arrancar — en *todo* proceso, tanto en la petición web como en el worker de cola — envuelve cada invocación de destino en el decorador de reintento/DLQ de más abajo, y llama a `$bus->subscribe($pattern, $wrapped)` por cada patrón, resolviendo el bean de destino **fresco desde el contenedor en cada despacho**.

---

Expand Down
2 changes: 1 addition & 1 deletion book/src-es/09-transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ Cuatro detalles, cada uno de los cuales acabarás necesitando:

El Capítulo 2 te enseñó la forma de una clase `#[Configuration]` y sus métodos fábrica `#[Bean]`. Durante una versión de este framework, toda aplicación LaraFly tuvo que escribir una de ellas a mano — un `#[Configuration]` cuyo único `#[Bean]` cargaba el `TransactionalManifest` cacheado — o `#[Transactional]` no hacía absolutamente nada. **Ya no necesitas esa clase**, y merece una página: el fallo que aquella clase sorteaba fue de los más afilados que ha tenido este framework, y su arreglo es una pequeña lección sobre cómo se resuelve cada artefacto compilado.

`php artisan firefly:cache` emite **tres** artefactos separados que lee la maquinaria de proxies. Ejecuta `TransactionalScanner`/`TransactionalManifestCompiler` — uno de los trece pares scanner/compiler — para escribir los datos del manifiesto en `bootstrap/cache/firefly/transactional.php`. Ejecuta `ProxyPlanCompiler` sobre el `ProxyPlan` fusionado para escribir `bootstrap/cache/firefly/proxy-plan.php` — el artefacto que decide qué beans reciben siquiera un proxy y qué advice ejecuta cada uno de sus métodos. Y ejecuta `ProxyClassGenerator` para emitir un archivo fuente `{Target}__FireflyTransactionalProxy` **por cada clase que el plan nombra** — cada clase que reclama algún `AdviceSource`, así que un bean que solo lleva reglas de seguridad de método también recibe uno — en un directorio `proxies/`, más un classmap `proxies.php`.
`php artisan firefly:cache` emite **tres** artefactos separados que lee la maquinaria de proxies. Ejecuta `TransactionalScanner`/`TransactionalManifestCompiler` — uno de los catorce pares scanner/compiler — para escribir los datos del manifiesto en `bootstrap/cache/firefly/transactional.php`. Ejecuta `ProxyPlanCompiler` sobre el `ProxyPlan` fusionado para escribir `bootstrap/cache/firefly/proxy-plan.php` — el artefacto que decide qué beans reciben siquiera un proxy y qué advice ejecuta cada uno de sus métodos. Y ejecuta `ProxyClassGenerator` para emitir un archivo fuente `{Target}__FireflyTransactionalProxy` **por cada clase que el plan nombra** — cada clase que reclama algún `AdviceSource`, así que un bean que solo lleva reglas de seguridad de método también recibe uno — en un directorio `proxies/`, más un classmap `proxies.php`.

Durante mucho tiempo, nada cargaba el primero de esos tres. `DataAutoConfiguration` vinculaba un `TransactionalManifest` *vacío* incondicionalmente, el `transactional.php` compilado solo lo referenciaba su propia declaración, y por eso `hasProxyFor()` era siempre falso y `TransactionalBeanPostProcessor` devolvía cada bean sin envolver. `#[Transactional]` era un **no-op silencioso** en cualquier aplicación que no escribiera a mano su propia configuración de manifiesto — que es exactamente lo que aquel `#[Configuration]` manuscrito existía para hacer, y exactamente el modo de fallo del que avisaba la sección anterior, llegando desde el propio lado del framework.

Expand Down
Loading
Loading