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.
Architecture fit. This is not a runtime component — it is a PHPStan analysis extension that teaches static analysis to reason about patchlevel/event-sourcing aggregate internals. Architecturally it is a leaf-node dev dependency: it hooks into PHPStan's rule registry via extension.neon, contributes custom rules and type extensions for AggregateRoot/ChildAggregate subclasses, and ships no runtime code, no service providers, no config publishes, and no migrations. That makes it trivially removable and impossible to cause a production incident. The only architectural precondition is that the codebase already uses patchlevel/event-sourcing with aggregates that follow the library's apply/recordThat conventions — without that it is inert.
Integration feasibility. Very high feasibility and very low effort: one composer require --dev, plus a single includes: line in phpstan.neon unless phpstan/extension-installer is present (in which case it self-registers). The genuinely valuable rules from a product-risk standpoint are the two correctness guardrails: DontRecordWhenApplying (recording events inside apply produces duplicated events on replay — a data-integrity bug that is expensive to unwind post-hoc) and DontWriteStateWhenNotApplying (state written outside apply silently vanishes on reload — a correctness bug that typically surfaces as intermittent, hard-to-reproduce invariant violations). The property-initialization feature is a developer-experience win that removes the false positives that otherwise force teams to weaken PHPStan levels or litter @phpstan-ignore on aggregates. The unused/write-only property rules are design-hygiene checks that double as architectural guidance toward projections.
Technical risk. Low but non-zero, and concentrated in one place: version pairing with the host library. The rules match on the library's API surface and base-class types. If patchlevel/event-sourcing is upgraded without upgrading this extension, the failure mode is silent — the rules stop matching and report nothing, which is worse than an error because CI stays green while the guardrails are gone. This must be mitigated by treating the two packages as a lockstep pair in the dependency policy. Secondary risks: a single-maintainer, 1-star, 0-dependent package with a correspondingly thin bus factor (mitigated by MIT license and tiny scope — forkable in an afternoon); false positives on nested/child aggregates and on properties read only through dynamic access (__get, reflection, magic methods); and baseline churn on every extension minor bump, since new rules (the jump from 1.0 to 1.3 added three rule families) invalidate an existing PHPStan baseline.
Key questions.
patchlevel/event-sourcing in production, and how many aggregate classes are in scope? This is the binary go/no-go.level: max on aggregates) apply, or are we already at a low level where the extension's benefit is marginal?event-sourcing, and is that encoded in a Renovate/Dependabot grouping rule rather than a human's memory?Stack fit. Direct fit for any PHP project running patchlevel/event-sourcing — PHPStan is already the de-facto standard analyzer in Laravel and Symfony shops, and phpstan/extension-installer is commonly present, which reduces onboarding to a single composer command. No Laravel service provider, no Octane/queue/container interaction, no production surface. It belongs in the require-dev block of the root composer.json, not in shared internal packages, unless an internal package ships aggregates — in which case it belongs there too.
Migration path. Baseline-first, then ratchet:
extension.neon include to the primary PHPStan config.vendor/bin/phpstan analyse and inspect the delta rather than blindly regenerating. Expect three classes of finding: (a) false positives on initialization, which the extension should actually be removing — a net reduction in errors; (b) real recording-in-apply and write-state-outside-apply violations, which are correctness bugs and should be fixed, not baselined; (c) unused/write-only property findings, which are design debt and can be baselined initially.phpstan-tests.neon, per-package configs, monorepo splits). A missing include in one config is the single most common integration failure and produces a false sense of coverage.Compatibility. Requires PHPStan (and phpstan/extension-installer if used) at compatible versions, and — critically — an event-sourcing major/minor line that the extension's rules were written against. The 1.1.0 release dropped DCB-related checks because they never landed in event-sourcing 3.x and added PHP 8.5 support, which is a concrete illustration of how tightly the two version ranges track each other. Pin both, upgrade both, and re-run analysis before merging either bump. Also verify the includes: path resolves if a non-standard config.vendor-dir is configured.
Sequencing. Cheap and non-blocking, so sequence it early and in parallel with other work:
apply assigns state only) so new aggregates are correct by construction.event-sourcing in dependency automation so they are never upgraded independently.Maintenance. Negligible day-to-day cost: no services, no runtime, no data. The real maintenance obligation is policy, not code — keeping the extension and event-sourcing on matched version ranges and re-validating after each bump. Budget a small recurring allowance (roughly an hour per library upgrade cycle) for PHPStan runs, baseline review, and the deliberate-violation smoke test. Rule additions in minor releases (three new rule families landed between 1.0 and 1.3) will surface new findings, so treat each extension upgrade as a code-review event rather than an automated merge.
Support. Upstream support is effectively single-maintainer; with 1 star and zero dependents there is no community gravity to lean on. The mitigations are structural: MIT license, tiny surface area, and rules that are individually configurable (made so in 1.3.0 per PR #29), meaning you can disable one noisy rule without forking. If the package is ever abandoned, the escape hatch is to vendor the rule classes into an internal tooling repo — a day of work, not a quarter. Recommended posture: adopt, but do not build organizational dependency on its roadmap.
Scaling. Scales well and linearly with codebase size — PHPStan already dominates CI wall-clock time, and the extension's marginal cost is proportional to the number of aggregate classes only. Two scaling pinch points to watch: (a) result-cache invalidation on extension upgrades causes full re-analysis passes, which is noticeable in large monorepos and worth scheduling off-peak; (b) false-positive volume grows with the number of non-standard aggregate shapes (nested aggregates, magic access, trait-composed state), so the cost of adoption is disproportionately higher in legacy domains than in greenfield ones. Scope the initial rollout to the cleanest bounded context and expand outward.
Failure modes. Ranked by impact:
| Failure | Symptom | Impact | Mitigation |
|---|---|---|---|
Silent rule mismatch after event-sourcing upgrade |
Rules match nothing; CI green; guardrails gone | High — false confidence in correctness | Lockstep version pinning + planted-violation smoke test in CI |
Missing includes: in a secondary config |
Partial coverage; tests/fixtures unanalyzed | Medium | Single shared neon file included everywhere; audit configs on change |
| Message-regex ignores | Ignores stop matching after reword; and suppress unrelated future errors | Medium | Identifier-based ignoreErrors only |
| Stale baseline after minor bump | New real findings masked as pre-existing | Medium | Regenerate deliberately, reviewing new entries, never blind-regenerate |
| False positives on child aggregates / dynamic reads | Developer friction; pressure to disable rules globally | Medium | Identifier-scoped ignores with rationale comments; keep typed getters on aggregates |
| Upstream abandonment | No fixes for new PHP/PHPStan versions | Low | MIT + tiny scope; fork-in-place fallback |
Ramp-up. Near-zero for anyone who already knows PHPStan — the mental model is "these are extra error identifiers." The real ramp-up is domain education, not tooling: developers must internalize why recording inside apply duplicates events and why write-only aggregate state belongs in a projection. Budget a 30-minute walkthrough and a one-page convention note in the contributing docs. The extension is unusually effective as a teaching device here, because every violation carries an explanation and a fix — treat it as an opinionated reviewer for event sourcing design, not merely a linter. New joiners get the benefit passively, since correctly structured aggregates pass and incorrect ones fail loudly in CI.
How can I help you explore Laravel packages today?