patchlevel/event-sourcing-phpstan-extension
PHPStan extension for patchlevel/event-sourcing that validates aggregates before runtime: flags uninitialized and unused properties, write-only state, event recording inside apply methods, and state changes outside apply.
patchlevel/event-sourcing. It doesn't add runtime capability; it converts a class of event sourcing bugs from "caught in review or production" to "caught in CI." If the team is already standardizing on the Patchlevel event sourcing stack, this is effectively a free correctness upgrade — no build cost beyond a config line, no lock-in beyond a dev dependency.level: max — a quality-gate decision with real ongoing value.apply) are not lint nits — they encode architectural intent. Write-only state usually belongs in a projection; unwritten state usually means a typo or an unwired event; recording inside apply produces duplicated events on replay. Adopting the extension converts a domain-modeling checklist that reviewers must remember into an automated gate. That reduces the senior-reviewer bottleneck on event sourcing PRs.apply, recordThat, aggregate base classes). That creates a coupling: library upgrades and extension upgrades should land together, plus a baseline regeneration step. This is a small, recurring process cost that belongs in the upgrade runbook, not a one-time install.Adopt when all of the following hold:
patchlevel/event-sourcing in production or in a project committing to it. The extension is inert on any other codebase.apply* methods and invariants — the rules are most valuable where aggregates carry real state.Look elsewhere or defer when:
__get, reflection, dynamic property access). The unused/write-only rules rely on PHPStan's read/write analysis and will produce false positives or silences. Prefer typed getters first, or expect to manage identifier-scoped ignores.patchlevel/event-sourcing changes shape, this repo follows or lags. Pin the version and treat it as a maintained-by-vendor asset whose continuity is tied to the library's.For executives: This is a small, free, dev-only add-on for teams using the Patchlevel event sourcing library. It turns a category of subtle, expensive bugs — state that silently disappears when an aggregate is reloaded, and duplicate events on replay — from "discovered in production or in a painful debugging session" into "flagged automatically in CI before merge." There is no runtime footprint, no license cost, and no user-facing change. The cost is a config line plus a small ongoing habit of keeping it version-matched with the library. The payoff is fewer senior-reviewer hours spent manually checking event sourcing conventions and a foundation for raising code-quality gates on domain code. Recommended as a default for any team already on this library, and as a deliberate non-decision for teams that aren't.
For engineering: Install as a dev dependency, register extension.neon in every PHPStan config that scans aggregates (app and tests), and run it at a high strictness level — this extension is the thing that unblocks level: max on aggregates by removing false "uninitialized property" reports. It ships five rules: property initialization for roots and child aggregates, unused properties (no apply writes them), write-only properties (written but never read — usually a projection that leaked onto the aggregate), recording inside apply (causes duplicated events on replay), and state writes outside apply (state lost on reload). All rules are configurable.
Three integration realities to plan for. First, a first run on an existing codebase produces a burst of findings — baseline by error identifier, never by message regex, and burn the baseline down incrementally; regenerate (and inspect) it after every upgrade rather than blindly. Second, version-pair with patchlevel/event-sourcing and re-run PHPStan in the same PR as any library bump, because a mismatch can make the rules silently stop matching your aggregates — worse than a loud failure. Third, verify it's actually running by introducing a deliberate violation in a throwaway aggregate and confirming it's flagged; forgetting the includes: line is the most common reason the extension "doesn't work."
Treat the findings as design feedback, not noise. A write-only property wants to be deleted or moved to a read model. An unwritten property wants a typo fixed or a missing event wired up. Structure new aggregates to the pattern the rules enforce — command method validates → records event → framework calls apply → apply only assigns properties — and the rules stop being a checker and start being a design guide. Note the caveat: single contributor, one star, no dependents — pin the version, and treat it as a vendor asset that moves with the library rather than an independent community dependency.
How can I help you explore Laravel packages today?