Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Doctrine Projections Laravel Package

darangonaut/laravel-doctrine-projections

A Laravel package for Doctrine projections that helps you build and maintain read models from domain events. It integrates projection classes with Laravel’s container and console, making event-sourced read-model setup and rebuilding straightforward.

View on GitHub
Deep Wiki
Context7

Getting Started

This package generates Eloquent "projection" models from your Doctrine ORM mappings, so reads can go through Eloquent (chunk, cursor, lazy, paginate, route model binding, replicate, toArray, toJson) while writes stay in Doctrine. Read the UPGRADING.md first — 0.7.0 is a minor with three hard refusals, not a patch — then the generated-output docs.

Minimal path:

  1. Require the package on PHP 8.3/8.4 and Laravel 12/13 (the tested matrix: SQLite, MySQL 8.4, PostgreSQL 16).
  2. Publish the config and point the projections namespace/path at a directory that your entities do not live in. As of 0.7.0 a colliding namespace is refused outright (previously the generated model took the entity's FQCN and the autoloader picked a winner at random).
  3. Run the generate command. It needs no database — it reads mapping, not schema — so it runs fine on a bare CI container. 200 entities generate in ~0.03s, which is cheap enough to run on every build.
  4. Commit the generated models, then use them immediately for a read-heavy screen: an index page with paginate(), a report with cursor(), an API resource with toJson().

Your first real use case is almost always replacing a hand-written Doctrine DQL query on a listing page with Projection::query()->paginate(). Do that one screen, confirm the generated models survive Model::shouldBeStrict() in dev, then widen.

Implementation Patterns

Read/write split by construction. Doctrine owns the entity and the unit of work; projections own the query surface. Keep that boundary literal — do not add mutators, scopes, or relationships to generated models, because regeneration will eat them. If you need custom query logic, put it in a repository or query object that takes the projection class as a parameter rather than subclassing the generated model.

Treat generated output as a build artifact with a drift check. Because generation is DB-free and sub-second, the strongest pattern is: regenerate in CI and git diff --exit-code on the output directory. That turns "someone changed a mapping and forgot to regenerate" into a red build instead of a runtime Column not found.

Migration generation is a separate, deliberate step. The migration name is timestamp-to-the-second + --name, defaulting to doctrine_diff. Two ordinary runs in the same second used to resolve to one path; 0.7.0 refuses to overwrite instead. So: never generate migrations in a loop or in CI, and always pass an explicit --name when you generate them (e.g. per-feature-branch), so parallel work doesn't collide on the default.

Single-table inheritance: pick the right entry point. The root class stays unscoped — "every node" is a legitimate question — and everything below it is scoped to its own discriminator value plus its subclasses'. So:

Intent Query
Every row in the table, all types root projection
One concrete type that subclass
A mid-hierarchy abstract type and its children that abstract class (now correctly scoped)

Prefer querying the most specific class that expresses the intent; reaching for the root when you meant one type is the most common way to leak rows into a listing.

Regeneration workflow per mapping change: change the Doctrine mapping → regenerate → drift check passes → if the schema moved, generate a migration with an explicit name → review the migration by hand (it's a diff, not a design). Projections follow mappings, migrations follow schema; they are not the same trigger.

Strict mode in dev. The verified Eloquent surface — chunk, cursor, lazy, paginate, route model binding, replicate, toArray, toJson — is tested under Model::shouldBeStrict(). Enable it locally so N+1s and missing-attribute access surface before production.

Gotchas and Tips

Audit aggregate numbers after upgrading to 0.7.0. The silent fix: a mid-hierarchy abstract class used to have no scope and returned every row in the table (AbstractFile::query()->count() → 4, including a folder; findAll() → 3). It is now scoped. Any dashboard, export, or count that queried an abstract subclass will drop after upgrade. Confirm the new number is the one you meant, and switch to the root projection only where "every node" is genuinely the question.

Generation failure was silent; it no longer is — but check your exit codes anyway. File::put() returns false with Laravel's error handler off and throws a raw ErrorException with it on. Before 0.7.0 a run that wrote nothing still printed "3 projection(s) generated" and exited 0. Your deploy scripts should now assert on exit status and on the drift check; a green pipeline that produced no files is exactly the failure this release closed.

Three new refusals will stop previously "working" runs:

  • Projection namespace overlapping an entity namespace — move the output path; do not try to share it.
  • Two entities whose short names differ only in case (Order.php / order.php) — one file on macOS and Windows. Rename one; this is a portability bug, not a package quirk.
  • Overwriting an existing migration — pass --name, and don't regenerate in the same second.

Root is unscoped, deliberately. Root::query() includes every subclass row. That's correct behaviour, not a leak — but it means a root query with an ad-hoc where type is a smell; you probably wanted the subclass.

Three mappings are documented as broken, not guarded: a non-backed enum, enumType on a json column, and a discriminator column mapped as a field. Fix the mapping; the package will not catch these for you.

Pre-1.0 discipline. Every 0.x minor can refuse something that used to run. Read UPGRADING.md on every bump, pin exact versions in composer.json, and keep the regenerate-and-diff check so a bump that changes generated output shows up as a reviewable diff rather than a production surprise.

Debugging order when a projection misbehaves: (1) is the generated file stale — regenerate and diff; (2) is the class scoped as you expect — check where it sits in the hierarchy; (3) is the underlying mapping one of the three known-broken shapes; (4) only then look at the package. PHPStan level max with no baseline is a good signal that a static-analysis pass on your own projection-consuming code will catch most attribute and typing mistakes.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log