Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
da56def
feat(lumen): add an event-sourced, snapshot-aware Loan aggregate with…
Oct 13, 2026
b3dbaa7
test(lumen): store loans in the database event store, replay them fro…
Oct 13, 2026
def8309
feat(lumen): grant and repay loans through the bus, appending events …
Oct 13, 2026
c73d8bd
feat(lumen): project loan events into a checkpointed loan book that t…
Oct 13, 2026
2794636
feat(lumen): upcast loan.repaid v1 to v2 on read, so the repository a…
Oct 13, 2026
dc7e956
test(lumen): publish loan events through the database outbox in the a…
Oct 13, 2026
748806d
test(lumen): run the payout saga on the event-sourced orchestration s…
Oct 13, 2026
6f2e12a
test(lumen): run the event-sourced loan and the event-sourced payout …
Oct 13, 2026
cca8624
test(lumen): keep the integration trait's setUp and resolveApplicatio…
Oct 13, 2026
9e281d3
docs(lumen): describe the event-sourced loan, its projection, upcaste…
Oct 13, 2026
e58ca52
test(docs): boot the tutorial's event-sourced greeting journal and it…
Oct 13, 2026
1a0eb7c
docs(book): add Chapter 9B, Lumen's event-sourced loans with snapshot…
Oct 13, 2026
1091cb4
docs(book): the preface, Chapter 9A, Appendix A and the glossary name…
Oct 13, 2026
216171d
docs(tutorial): Step 14 records greetings as events and projects them…
Oct 13, 2026
6af21a8
docs: the front pages name event sourcing and point at Chapter 9B and…
Oct 13, 2026
2947582
docs(changelog): record the D2 event-sourcing chapter, tutorial step …
Oct 13, 2026
98e46fb
fix(lumen): debit a wallet only for its owner or an admin, through a …
Oct 13, 2026
8487269
docs(book): Chapter 10 checks wallet ownership with hasPermission and…
Oct 13, 2026
2acfaf6
docs(changelog): record the Lumen wallet-ownership fix under [Unrelea…
Oct 13, 2026
58f9827
fix(lumen): guard TransferHandler with the wallet-ownership rule, so …
Oct 13, 2026
93f0592
docs(book): TransferHandler carries the ownership guard in Chapters 6…
Oct 13, 2026
52212f6
fix(cqrs): CommandProcessingException and QueryProcessingException ca…
Oct 13, 2026
3fd6151
docs: the CQRS wrappers carry the cause's extension members, so Chapt…
Oct 13, 2026
d080ae2
docs(changelog): record the Lumen transfer guard and the CQRS wrapper…
Oct 13, 2026
d618cc4
test(lumen): clear the security context only for a booted lane, so a …
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
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,22 @@ All notable changes to LaraFly are documented here. This project uses CalVer (`Y
endpoint; partner callbacks through `firefly/callbacks`; `GetBalance` as a `#[CachedQuery]` evicted per wallet by
`#[InvalidatesQuery]` on `FundsDeposited`/`FundsWithdrawn`; and the portable outbox, proven on PostgreSQL, MySQL,
MariaDB, SQL Server and SQLite by `OutboxDatabaseRoundTripTest`.
- **Book: Chapter 9B, in English and Spanish.** *Event Sourcing: Storing What Happened* walks Lumen's event-sourced
loans — `#[EventType]` events, an `EventSourcedAggregateRoot` replayed from history and from snapshots, the 409 a
stale writer gets, one transaction with the wallet, a checkpointed `loan_book` projection run, listed and rebuilt by
the projection commands, an upcaster for a renamed field, publication through the outbox in the append's own
transaction, and the payout saga on `firefly.orchestration.persistence.provider=event-sourced` — slotted after
Chapter 9A without renumbering. The preface, Chapter 9A, Appendix A and the glossary (checkpoint, event sourcing,
event store, snapshot, upcaster) follow. The book now has nineteen chapters.
- **Tutorial Step 14, in English and Spanish.** The greeting app records every greeting as a `greeting.said` event
in the database event store and projects the counts into a `greeting_counts` read model at
`POST`/`GET /journal/{name}`; the step's files are quoted from `tests/Fixtures/Tutorial`, which
`tests/TutorialGreetingJournalTest.php` boots through the real web and eventsourcing pipeline. "What's Next" is now
Step 15.
- **`samples/lumen` exercises W3.** An event-sourced `Loan` with `GrantLoan`/`RepayLoan` handlers that move the money
through the wallet in the same transaction, `LoanRepository`, `LoanBookProjection`, `LoanRepaidV1Upcaster` and the
payout saga on the event-sourced orchestration store, proven on PostgreSQL, MySQL, MariaDB, SQL Server and SQLite
by `LoanLedgerRoundTripTest` and `EventSourcedPayoutRoundTripTest`.

### Changed

Expand All @@ -133,6 +149,23 @@ All notable changes to LaraFly are documented here. This project uses CalVer (`Y

### Fixed

- **The Lumen sample let any wallet owner debit any wallet.** `WithdrawHandler` and `RepayLoanHandler` were guarded
by `hasRole('ADMIN') or hasRole('WALLET_OWNER')`, so every customer could withdraw from, pay out of or repay a
loan against someone else's wallet. Both now require `hasRole('ADMIN') or (hasRole('WALLET_OWNER') and
hasPermission(#command, 'debit'))`, answered by the sample's `WalletPermissionEvaluator`, which grants the debit
only to the wallet's owner and refuses unknown targets and permissions; Chapters 4, 6, 9A, 9B and 10 of the book
follow, in both editions.
- **The Lumen sample let anyone, even an anonymous caller, transfer money out of any wallet.** `TransferHandler`
(`POST /api/v1/wallets/transfers`) had no `#[PreAuthorize]`. It now carries the same rule as `WithdrawHandler`,
and `WalletPermissionEvaluator` grants `debit` on a `Transfer` only to the owner of its source wallet: an
anonymous transfer is a 401, a non-owner's a 403, and neither moves money on either wallet. Chapters 6, 9 and 10
and the README follow in both editions, and Chapter 10's exercise 2, which asked the reader to add this guard, now
asks them to guard a read.
- **A refused command or query lost its `requiredAuthorities`.** `CommandProcessingException` and
`QueryProcessingException` copied the cause's code, status, category and severity but not its RFC 9457 extension
members or title, so the problem document of a `#[PreAuthorize]` refused at the bus dropped the
`requiredAuthorities` the same refusal publishes from a controller (and any handler fault lost its `field`,
`allowed` or own title). Both wrappers now carry them; only `detail`, which names the message class, differs.
- **`firefly:callbacks:replay` honors deactivation and endpoint changes.** The deliverer refuses an inactive
subscription on every path (replay needs `--include-inactive`, which the output reports), and every delivery goes
to the subscription's current URL — checked against `http.require-https` / `http.allowed-hosts` — instead of the
Expand Down
33 changes: 19 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ event-driven, actuator-observed microservice, chapter by chapter — every listi
**[Download the book in English or Spanish, as PDF or EPUB](https://fireflyframework.github.io/fireflyframework-php/book/).**
The published editions include the single-package installation and bundled installer.

The book is **structurally complete and bilingual (English + Spanish)**: a quick start, **eighteen chapters**
The book is **structurally complete and bilingual (English + Spanish)**: a quick start, **nineteen chapters**
across four parts — Foundations (DI, config, HTTP), Modelling & Persisting the Domain (repositories, DDD),
Coordinating & Securing the App (CQRS, EDA + transactional outbox, webhooks and callbacks, `#[Transactional]`, sagas/TCC/durable workflows, security, OAuth2 and
Coordinating & Securing the App (CQRS, EDA + transactional outbox, webhooks and callbacks, `#[Transactional]`, sagas/TCC/durable workflows, event sourcing, security, OAuth2 and
OpenID Connect), and Observability, Testing & Delivery (actuator, testing, the CLI + zero-reflection cache,
feature flags)
— plus a Laravel→LaraFly cheat-sheet and a glossary. The two editions are line-for-line the same book: every
Expand Down Expand Up @@ -158,7 +158,7 @@ LaraFly is not a fork of Laravel and does not hide it — every package layers c
`Illuminate\Container`, Eloquent, the HTTP kernel, and the queue/cache/scheduler, so everything you already
know about Laravel keeps working underneath.

Five capabilities in that list are recent enough — and far enough from what teams assume a PHP framework
Six capabilities in that list are recent enough — and far enough from what teams assume a PHP framework
will have — that they are worth naming outright:

- **Both halves of OAuth2, as two packages you install.**
Expand Down Expand Up @@ -196,11 +196,17 @@ will have — that they are worth naming outright:
- **Work that spans systems.** [`firefly/orchestration`](docs/modules/orchestration.md) runs sagas whose
completed steps are compensated newest first when a later one fails, TCC transactions that reserve with every
participant before confirming any, and durable workflows that wait for signals and timers across processes —
every step checkpointed (memory, cache or a portable database store), stale executions recovered, failures
every step checkpointed (memory, cache, a portable database store or the event store), stale executions recovered, failures
dead-lettered. [`firefly/webhooks`](docs/modules/webhooks.md) verifies Stripe, GitHub, Twilio or HMAC
signatures, refuses replays and duplicates and publishes the rest to the event bus;
[`firefly/callbacks`](docs/modules/callbacks.md) sends the bus's events to subscribers as signed, retried POSTs
behind a circuit breaker per endpoint.
- **Event sourcing, on any database.** [`firefly/eventsourcing`](docs/modules/eventsourcing.md) stores an
aggregate as its events — appended under optimistic concurrency to a portable event store, read back from the
newest snapshot, moved forward by upcasters when an event's shape changes — and keeps read models current from a
global stream numbered in commit order, so a projection's checkpoint never skips an event that committed late.
Appended events can be published through the database outbox in the append's own transaction, and sagas,
TCCs and workflows can store their executions as event streams.

### Who is LaraFly for?

Expand Down Expand Up @@ -774,6 +780,7 @@ class TransferHandler
{
public function __construct(private readonly WalletRepository $wallets) {}

#[PreAuthorize("hasRole('ADMIN') or (hasRole('WALLET_OWNER') and hasPermission(#command, 'debit'))")]
#[Transactional(propagation: Propagation::REQUIRED)]
public function handle(Transfer $command): void
{
Expand Down Expand Up @@ -812,7 +819,7 @@ class WithdrawHandler
{
public function __construct(private readonly WalletRepository $wallets) {}

#[PreAuthorize("hasRole('ADMIN') or hasRole('WALLET_OWNER')")]
#[PreAuthorize("hasRole('ADMIN') or (hasRole('WALLET_OWNER') and hasPermission(#command, 'debit'))")]
#[Transactional]
public function handle(Withdraw $command): int
{
Expand All @@ -828,7 +835,10 @@ class WithdrawHandler
```

`SecurityCommandAuthorizer` enforces this **before** the handler runs, at the bus — a denied withdraw surfaces
as a `CommandProcessingException` wrapping an `AuthorizationException` (403), never a silent no-op. The same
as a `CommandProcessingException` wrapping an `AuthorizationException` (403), never a silent no-op. The role
alone is not ownership: `hasPermission(#command, 'debit')` hands the `Withdraw` to the application's
`PermissionEvaluator` bean (Lumen's `WalletPermissionEvaluator`), which grants the debit only when the
principal owns that wallet, so one customer cannot debit another's. The same
rules hold on the controller dispatcher and on **any stereotyped bean**, through the one compiled proxy chain
`#[Transactional]` already used — so a `#[Service]` method carrying `#[PreAuthorize]`, `#[PostAuthorize]`,
`#[PreFilter]` or `#[PostFilter]` is guarded wherever it is called from, and a refusal never opens a
Expand Down Expand Up @@ -1142,7 +1152,7 @@ Start at the **[documentation table of contents](docs/README.md)** — it groups
- [Laravel ↔ Spring Boot Comparison](docs/laravel-comparison.md) — concept-by-concept mapping for both audiences.
- [Versioning](docs/versioning.md) · [Contributing](docs/contributing.md) · [Publishing](docs/publishing.md).
- Every [module guide](#modules) above.
- [*larafly by example*](book/README.md) — the complete bilingual book (18 chapters + appendices, PDF + EPUB).
- [*larafly by example*](book/README.md) — the complete bilingual book (19 chapters + appendices, PDF + EPUB).
- [`samples/lumen/`](samples/lumen/) — the wallet-and-ledger sample this README's showcases are drawn from;
run its own test suite with `vendor/bin/pest samples/lumen/tests`.

Expand Down Expand Up @@ -1175,13 +1185,8 @@ 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.
- **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
- **Documentation.** The end-to-end [tutorial](docs/tutorial.md) (EN + ES, through a database outbox, a saga and an event-sourced journal), the *larafly by example*
[book](book/README.md) (19 chapters + appendices, EN + ES, PDF + EPUB), and a
[docs table of contents](docs/README.md) all shipped with the documentation-parity milestone.
Deeper guides (more recipes, more diagrams) continue to grow from here.

Expand Down
11 changes: 7 additions & 4 deletions book/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ book/
src/ # EN manuscript (Markdown)
00-front/ # title/copyright/dedication/preface/conventions
00-quickstart.md # "Build Lumen step by step" quick start
01..13a-*.md # the eighteen chapters, 4A, 8A, 9A, 10A and 13A included (Parts I-IV)
01..13a-*.md # the nineteen chapters, 4A, 8A, 9A, 9B, 10A and 13A included (Parts I-IV)
90-appendix-a-laravel.md # Laravel -> LaraFly cheat-sheet
94-glossary.md # glossary
src-es/ # ES manuscript, same structure/filenames
Expand All @@ -139,7 +139,7 @@ book/
## Manuscript status

The manuscript is **structurally complete** in both languages: a five-file front
matter, a "Build Lumen step by step" quick start, eighteen chapters across four
matter, a "Build Lumen step by step" quick start, nineteen chapters across four
parts —

- **Part I — Foundations**: Why LaraFly, Dependency Injection & Auto-Configuration,
Expand All @@ -148,7 +148,7 @@ parts —
Domain-Driven Design
- **Part III — Coordinating & Securing the Application**: CQRS, Event-Driven
Architecture & the Transactional Outbox, Webhooks and Callbacks, Transactions & the `#[Transactional]`
proxy, Sagas, TCC and Durable Workflows, Security, OAuth2 and OpenID Connect
proxy, Sagas, TCC and Durable Workflows, Event Sourcing, Security, OAuth2 and OpenID Connect
- **Part IV — Observability, Testing & Delivery**: Observability/Actuator,
Testing, the CLI & the Zero-Reflection Cache, Feature Flags

Expand All @@ -161,7 +161,10 @@ the reader's own code, which is the kind `php -l` checks. Both editions build to
`book/dist/` as PDF + EPUB. Chapters 8A and 9A walk the code W1, W2 and W4
added to Lumen — `CardTopUpListener`, the partner callbacks, `PayoutSaga` and
`SimulatedBankGateway` — and every listing of that code is a `source:` excerpt
of the file the sample's own tests run.
of the file the sample's own tests run. Chapter 9B does the same for W3's event
sourcing — `Loan`, `LoanRepository`, `LoanBookProjection` and
`LoanRepaidV1Upcaster` are quoted from the sample, whose tests also run them on
every integration lane.

**The two editions are the same book, line for line.** Every chapter file has
the same sections in the same order at the same line numbers, and every `php`
Expand Down
5 changes: 5 additions & 0 deletions book/book.es.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ labels:
# sale de ESTA lista, así que los Capítulos 9-13A conservan su número y siguen siendo ciertas todas las referencias
# "Capítulo N" de ambos manuscritos y de book/README.md. Webhooks y callbacks comparten capítulo porque son las dos
# direcciones de un mismo contrato: la firma de un callback de LaraFly es exactamente la que comprueba el validador `hmac`.
# Tarea D2: firefly/eventsourcing añadido como Capítulo 9B, justo después del Capítulo 9A, cuyas ejecuciones puede
# guardar como flujos de eventos, y del capítulo de transacciones dentro de cuya única frontera añade sus eventos.
# Intercalado exactamente igual que el 4A, el 8A, el 9A y el 10A: `num` es una etiqueta libre y el orden sale de ESTA
# lista, así que los Capítulos 10-13A conservan su número y cada referencia cruzada "Capítulo N" sigue siendo cierta.
front:
- {id: title, file: 00-front/00-title.md, nav: false}
- {id: copyright, file: 00-front/00-copyright.md, nav: false}
Expand Down Expand Up @@ -83,6 +87,7 @@ parts:
- {id: ch08a, file: 08a-webhooks-callbacks.md, num: "8A", title: "Webhooks y Callbacks: Recibir y Enviar Eventos Firmados"}
- {id: ch09, file: 09-transactions.md, num: 9, title: "Transacciones y el Proxy #[Transactional]"}
- {id: ch09a, file: 09a-orchestration.md, num: "9A", title: "Sagas, TCC y Workflows Duraderos"}
- {id: ch09b, file: 09b-eventsourcing.md, num: "9B", title: "Event Sourcing: guardar lo que ocurrió"}
- {id: ch10, file: 10-security.md, num: 10, title: "Seguridad: Autenticación y Autorización"}
- {id: ch10a, file: 10a-oauth2.md, num: "10A", title: "OAuth2 y OpenID Connect: Iniciar Sesión, y Ser el Proveedor"}
- title: "Parte IV — Observabilidad, Pruebas y Entrega"
Expand Down
5 changes: 5 additions & 0 deletions book/book.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ labels:
# the order comes from THIS list, so Chapters 9-13A keep their numbers and every "Chapter N" cross-reference in both
# manuscripts and in book/README.md stays true. Webhooks and callbacks share one chapter because they are the two
# directions of one contract — LaraFly's callback signature is exactly what the webhooks `hmac` validator checks.
# Task D2: firefly/eventsourcing added as Chapter 9B, immediately after Chapter 9A, whose executions it can store as
# event streams, and after the transactions chapter whose single boundary it appends inside. Slotted exactly the way
# 4A, 8A, 9A and 10A were: `num` is a free-form label and the order comes from THIS list, so Chapters 10-13A keep their
# numbers and every "Chapter N" cross-reference in both manuscripts and in book/README.md stays true.
front:
- {id: title, file: 00-front/00-title.md, nav: false}
- {id: copyright, file: 00-front/00-copyright.md, nav: false}
Expand Down Expand Up @@ -81,6 +85,7 @@ parts:
- {id: ch08a, file: 08a-webhooks-callbacks.md, num: "8A", title: "Webhooks and Callbacks: Receiving and Sending Signed Events"}
- {id: ch09, file: 09-transactions.md, num: 9, title: "Transactions and the #[Transactional] Proxy"}
- {id: ch09a, file: 09a-orchestration.md, num: "9A", title: "Sagas, TCC and Durable Workflows"}
- {id: ch09b, file: 09b-eventsourcing.md, num: "9B", title: "Event Sourcing: Storing What Happened"}
- {id: ch10, file: 10-security.md, num: 10, title: "Security: Authentication and Authorization"}
- {id: ch10a, file: 10a-oauth2.md, num: "10A", title: "OAuth2 and OpenID Connect: Signing In, and Being the Provider"}
- title: "Part IV — Observability, Testing & Delivery"
Expand Down
2 changes: 2 additions & 0 deletions book/src-es/00-front/00-preface.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ El recorrido empieza con suavidad. El **Inicio rápido** te lleva desde el insta

Los Capítulos 8A y 9A llevan a Lumen más allá de una sola base de datos: un procesador de tarjetas recarga wallets mediante un webhook firmado, un socio se entera de cada movimiento de dinero mediante callbacks firmados, y un pago saliente a un banco se ejecuta como una saga que se deshace sola, paso a paso, cuando el banco dice que no.

El Capítulo 9B guarda otro tipo de registro: los préstamos de Lumen se almacenan como los eventos que les ocurrieron — concedido, amortizado, liquidado —, se reconstruyen desde esa historia y desde instantáneas, se proyectan en una cartera de préstamos y se publican por el outbox en la misma transacción que el monedero a través del que mueven el dinero.

El Capítulo 13A cierra el recorrido de producción con feature flags: Lumen mantiene apagada una vista de la wallet, selecciona una cohorte compartida y prueba la ruta y el ciclo del override contra el framework real.

Cuando hayas terminado de trabajar todo el libro, tendrás un modelo mental de cada capa de un servicio LaraFly de producción, y una aplicación real y probada que lo demuestre.
Expand Down
Loading
Loading