docs: add Laravel and Symfony projection quickstart examples#666
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
quickstart-examples/Laravel/Projection/{DatabaseReadModel,EloquentReadModel}andquickstart-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).Userdomain (UserWasRegistered,UserNameWasChanged,UserWasDeactivated) with#[NamedEvent]for stable on-disk identity.EloquentReadModel/EntityReadModeldemonstrate the ORM read-model-as-aggregate pattern: projection emits an array viaoutputChannelName, a string-routed#[CommandHandler]on the Eloquent/Doctrine entity handles load + mutate; Ecotone auto-saves.run_example.phpwalks the full lifecycle — delete → init → emit → query → reset → backfill → delete — with inlineAssertchecks proving rebuild-from-events works.quickstart-examples/composer.jsontests:ci.Pull Request Contribution Terms