Skip to content

docs: add Laravel and Symfony projection quickstart examples#666

Merged
dgafka merged 12 commits into
mainfrom
feat/laravel-projection-example
May 18, 2026
Merged

docs: add Laravel and Symfony projection quickstart examples#666
dgafka merged 12 commits into
mainfrom
feat/laravel-projection-example

Conversation

@dgafka
Copy link
Copy Markdown
Member

@dgafka dgafka commented May 18, 2026

Why is this change proposed?

To show Symfony and Laravel users how to build projections idiomatically — including the "auto-load + auto-save" sugar that stateful aggregates provide, applied to ORM-backed read models. The existing projection docs lean on raw SQL examples, which leaves Eloquent/Doctrine users guessing about the idiomatic approach. These four runnable quickstart examples answer that directly with two patterns per framework: a lean direct-SQL projection, and a stateful-aggregate read model that gives you the ORM sugar on the read side.

Description of Changes

  • New quickstart-examples/Laravel/Projection/{DatabaseReadModel,EloquentReadModel} and quickstart-examples/Symfony/Projection/{DatabaseReadModel,EntityReadModel} — each a self-contained app with bootstrap, run_example.php, and a beginner-aimed README (8 sections, 4 mermaid diagrams).
  • Shared event-sourced User domain (UserWasRegistered, UserNameWasChanged, UserWasDeactivated) with #[NamedEvent] for stable on-disk identity.
  • EloquentReadModel / EntityReadModel demonstrate the ORM read-model-as-aggregate pattern: projection emits an array via outputChannelName, a string-routed #[CommandHandler] on the Eloquent/Doctrine entity handles load + mutate; Ecotone auto-saves.
  • run_example.php walks the full lifecycle — delete → init → emit → query → reset → backfill → delete — with inline Assert checks proving rebuild-from-events works.
  • All four examples wired into root quickstart-examples/composer.json tests:ci.

Pull Request Contribution Terms

  • I have read and agree to the contribution terms outlined in CONTRIBUTING.

dgafka added 12 commits May 18, 2026 18:18
…nd EloquentReadModel)

Two runnable examples under quickstart-examples/Laravel/Projection/ that demonstrate the full
ProjectionV2 lifecycle (init → events → query → reset → backfill → delete) using a User
event-sourced aggregate with three domain events, showing direct-write and outputChannelName
patterns for read model construction.
… Laravel Projection examples

Adds beginner-friendly READMEs with 4 mermaid diagrams and 8-section skeletons for
DatabaseReadModel and EloquentReadModel, the top-level index README, CI entries in
quickstart-examples/composer.json tests:ci, Apache-2.0 licence headers on all PHP
source files, and storage/framework cache/ exclusions to prevent generated files
from being tracked by git.
Decouple stored event identity from PHP class names so the event stream
stays readable across renames/moves. Stable names: user.was_registered,
user.name_was_changed, user.was_deactivated. READMEs explain why this
matters for any event you intend to keep on disk.
Replace the InternalHandler writer + plain Eloquent model with a single
UserReadModel class in App\ReadModel that is both #[Aggregate] and an
Eloquent Model. The projection now emits commands via outputChannelName
(matched on command FQCN) to #[CommandHandler] methods on the aggregate;
Ecotone handles load + save automatically. This demonstrates the
auto-load/save "sugar" on the read side that stateful aggregates provide
on the write side. Required symfony/expression-language for the
identifierMapping payload.userId expression.
Replace the three Command DTO classes with plain associative arrays.
The projection now returns the row data as an array and routes via a
string outputChannelName matching the #[CommandHandler] routing key.
identifierMapping uses bracket expression syntax (payload['user_id'])
to read from the array on instance handlers. No DTO classes left.
… EntityReadModel)

Ports the two Laravel Projection examples to Symfony, providing complete
projection lifecycle examples using Doctrine DBAL (DatabaseReadModel) and
Doctrine ORM entities as stateful aggregates (EntityReadModel). Both examples
run cleanly inside docker and are wired into quickstart-examples/composer.json tests:ci.
…dentifierMapping

Project the user id as 'userId' (camelCase) so the array key matches the
aggregate's $userId PHP property; Ecotone now resolves the identifier
automatically and instance command handlers no longer need an explicit
identifierMapping. The Doctrine column name stays 'user_id' (DB convention).
Both EloquentReadModel and EntityReadModel READMEs now call out the
class-vs-array trade-off: arrays are dependency-free; typed command
classes give named fields and IDE/static-analysis support. Either form
reaches the same #[CommandHandler] over the same outputChannelName.
…ripts

run_example.php now walks six steps (delete → init → emit → query → reset → delete)
instead of seven. The backfill step exercised behaviour that is incompatible with
MySQL today (DDL inside the backfill transaction triggers an implicit commit), and
removing it lets three of the four examples run successfully on MySQL while keeping
the full picture on Postgres. READMEs updated: section 6 renamed to 'Reset vs Delete'
and a note added pointing readers at ecotone:projection:backfill for the historical-
event replay use case the script no longer demonstrates.
…for MySQL

Doctrine ORM in DBAL 4 always wraps flush() in a savepoint when nested in
an outer transaction, and that savepoint is silently dropped by any DDL
MySQL implicit-commits (CREATE TABLE, DROP TABLE). Two paths inside the
example previously triggered DDL during step 3's command flow: the Prooph
event store's lazy stream table creation, and the projection's
#[ProjectionInitialization] / #[ProjectionDelete] hooks re-running their
CREATE/DROP statements.

Pre-create the event stream in step 2 (outside any transaction) so the
first commandBus->send doesn't trigger DDL. Guard the projection's init
and delete hooks with createSchemaManager()->tablesExist() so they only
emit DDL when the schema actually needs changing.

All four projection examples now pass on both Postgres and MySQL.
@dgafka dgafka merged commit 622ce18 into main May 18, 2026
6 of 8 checks passed
@dgafka dgafka deleted the feat/laravel-projection-example branch May 18, 2026 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant