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.
Read-side / projection strategy (build vs. buy). This package is a build-vs-buy answer for the narrow but expensive job of turning Doctrine entity mappings into Eloquent models. In-house, that generator is a few weeks of engineering plus permanent maintenance against every Doctrine mapping edge case (single-table inheritance, mapped superclasses, self-referencing associations, composite keys). Buying it moves that maintenance burden to a dependency and lets the team spend the same time on product surface.
CQRS-lite without a rewrite. It supports the pragmatic architecture where Doctrine stays the write model and Eloquent powers the read model — admin panels, reporting screens, API resources, paginate/cursor/chunk, route model binding, toArray/toJson, replicate. That is a decision to not migrate the write side, which is usually a much larger roadmap item. Product-wise this unlocks read features (search, list views, exports) on a Doctrine codebase without a doctrine-to-Eloquent migration epic.
Phasing a Doctrine exit (or not committing to one). Teams that want to leave Doctrine eventually can move read paths first, ship user-visible value, and defer the risky write-path migration indefinitely. This package makes "migrate later, incrementally" a defensible roadmap position rather than a vague intention.
Event-sourced / derived read models. Generation is mapping-driven and requires no database — 200 entities in 0.03s, pinned by a test. That makes projection generation a cheap, deterministic step in CI and a viable fit for event-sourcing read models that must be rebuildable from scratch. A PM can treat regenerating the read side as a routine, low-cost operation rather than a maintenance window.
Correctness and release-confidence decisions this release informs. Three items in v0.7.0 are product-risk items, not just engineering ones:
| Release-note item | Product consequence |
|---|---|
| Abstract mid-hierarchy class returned every row (unscoped) | Read models could over-report — e.g. a "files" list silently including folders. User-visible wrong data, not a crash. |
| Failed write reported as "success", exit 0 | Deploys could go green with zero models generated. This is a supply-chain-style failure mode: no alert, silent outage of read features. |
| Three inputs now refused instead of silently allowed | Namespace collisions, case-only filename collisions (macOS/Windows), and migration overwrites were previously resolved by whichever file won. Now they fail loudly. |
That last row is the most PM-relevant: the failures were moved from runtime/data-corruption into build time. It is a quality-direction signal worth citing when justifying the dependency, and worth citing against complacency if the team was previously relying on the silent behaviour.
Roadmap input — version risk. At v0.7.0 this is pre-1.0 and the release notes are explicit that a minor was effectively breaking: three things that used to run now refuse. For roadmap planning that means: pin the version, budget a small upgrade task per minor bump, and read UPGRADING.md as part of the upgrade ticket. Do not place this on a critical path where an unreviewed bump can ship.
Adopt when:
UPGRADING.md before bumping.Look elsewhere when:
enumType on a json column, and a discriminator column mapped as a field are documented, not guarded. Those are yours to fix first; the package will not protect you from them.To executives: This is an infrastructure dependency that lets us ship read-facing features — admin screens, reporting, API resources, search and export views — on a Doctrine codebase without funding a full persistence migration. It replaces an in-house generator we would otherwise own forever, and it removes a class of silent bugs: in the previous release, a failed code-generation step reported success and exited cleanly, so a deploy could go green while read features were missing. The new version fails loudly instead. It is pre-1.0 and generates a small upgrade task per minor release; we pin the version and review the upgrade notes, which is normal for developer tooling at this stage. Cost is a dependency plus a few hours per upgrade; the alternative is weeks of build and permanent maintenance.
To engineering: v0.7.0 is a minor that behaves like a breaking change — three previously-accepted inputs now refuse: a projection namespace colliding with your entity namespaces, two entities whose short names differ only in case (one file on macOS/Windows), and overwriting an existing migration (the timestamp-to-the-second plus a default doctrine_diff name made same-second collisions likely). All three are in UPGRADING.md and all three convert silent corruption into a build failure, which is the right trade. Two real fixes shipped: an abstract class mid-hierarchy had no scope and returned every row in the table (now scoped below the root), and File::put() returning false or throwing ErrorException was previously unchecked, so a no-op run printed "3 projection(s) generated" and exited 0. Generation requires no database and is now test-pinned — 200 entities in 0.03s — so it belongs in CI. Verified unchanged and worth knowing is covered: self-referencing OneToOne/ManyToMany, two associations onto one entity, mapped superclasses, key-only entities, indexBy on a self-reference, fetch/cascade/orphanRemoval, nullable composite-key columns, and the Eloquent surface we actually use — chunk, cursor, lazy, paginate, route model binding, replicate, toArray, toJson — all under Model::shouldBeStrict(). Green on PHP 8.3/8.4 × Laravel 12/13 across SQLite, MySQL 8.4 and PostgreSQL 16, PHPStan level max with no baseline. Pin the version, put the upgrade in the ticket.
How can I help you explore Laravel packages today?