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

Event Sourcing Phpstan Extension Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a PHPStan rules extension for patchlevel/event-sourcing. It adds no runtime code — it teaches PHPStan the semantics of aggregates so your event-sourced domain classes are analyzed correctly. If you don't use patchlevel/event-sourcing (note: this is not spatie/laravel-event-sourcing), it will do nothing for you. Treat it as a dev-only quality gate you bolt onto an existing PHPStan/Larastan setup.

Install:

composer require --dev patchlevel/event-sourcing-phpstan-extension

Register it. With phpstan/extension-installer already in your project (common in Laravel apps using Larastan), no config is needed. Otherwise add to your PHPStan config:

includes:
    - vendor/patchlevel/event-sourcing-phpstan-extension/extension.neon

If you use Larastan, your config is usually phpstan.neon or phpstan.neon.dist including vendor/larastan/larastan/extension.neon; add this include alongside it.

Where to look first:

  1. vendor/patchlevel/event-sourcing-phpstan-extension/extension.neon — the rules registered and their parameters (the rules are configurable since 1.3.0).
  2. The src/ rule classes — each maps to one check: property initialization, unused properties, write-only properties, recording inside apply, and state writes outside apply.
  3. The upstream docs at patchlevel.dev/docs/event-sourcing-phpstan-extension/latest — each rule is documented with expected-failure examples.

First use case: run vendor/bin/phpstan analyse against an app that already uses patchlevel/event-sourcing. Expect a burst of findings in three buckets:

  • false "uninitialized property" errors on aggregate properties (now resolved — a net reduction in errors),
  • write-only properties (state written by apply but never read — usually leakage into what should be a projection),
  • recording-in-apply or state-writes-outside-apply (genuine bugs).

Pick one aggregate — ideally a recently written one — and work through its findings before widening to the whole domain.

Implementation Patterns

Baseline-first rollout. On a mature codebase, don't try to fix everything at once. Generate a PHPStan baseline after adding the extension, then burn it down module by module. Commit the baseline with the extension so CI passes immediately and the diff to "clean" is small and reviewed.

CI gating, scoped to the domain. Add PHPStan to your pipeline exactly as you run it locally — same config, same level, same includes. In a Laravel app, you often have one config but many contexts; ensure the domain/aggregate paths aren't excluded via excludePaths (Larastan users sometimes exclude app/Domain to cut noise — that defeats this extension).

Pair with a high PHPStan level. The property-initialization rule is specifically designed to suppress false positives that would otherwise push you to lower the level or sprinkle ignores on aggregates. This extension unblocks strict levels on event-sourced code. Aim for level: 6–max on the domain directory, even if the rest of the app sits lower — use per-path includes/level in a secondary neon.

Enforce the lifecycle as a workflow. Structure every aggregate command method as: validate → recordThat(...) → framework invokes apply* → apply* assigns properties. Never record inside apply, and never mutate state outside apply. Once the rules are on, this stops being a code-review convention and becomes a build failure. New aggregates should be written to satisfy the rules from day one — the findings become a design guide.

Use findings as architecture signals. A write-only property almost always means the data belongs in a read model/projection, not on the aggregate — state on an aggregate exists to enforce invariants, not to serve queries. An "unused property" (never written by any apply) usually means a typo, a renamed event, or a field that was never wired up. Treat both as refactor prompts rather than noise to silence.

Aggregate test fixtures need the extension too. Story tests, fixtures, and test-double aggregates are frequently in a separate PHPStan config (phpstan-tests.neon). Include the extension there as well — otherwise those files report uninitialized-property false positives or hide real recording-in-apply bugs.

IDE feedback loop. Registering the rules means PhpStorm's PHPStan integration surfaces them inline. If your team finds the noise overwhelming, use identifiers to tag and filter — see gotchas — rather than disabling the rules.

Gotchas and Tips

Install alone does nothing. The most common "it's not working" cause is a missing includes: entry (or extension-installer not being present). Verify by deliberately writing a recordThat inside an apply method in a throwaway aggregate; if PHPStan stays silent, the extension isn't loaded.

Version pairing with patchlevel/event-sourcing is critical. Rules key off the library's API shape (apply/recordThat, aggregate base classes, attributes). If you bump the event-sourcing library, bump this extension in the same PR, and re-run PHPStan before committing. A mismatch may cause rules to silently stop matching — a false sense of safety that is worse than a loud failure. The 1.1.0 release, for example, removed DCB support because it never landed in event-sourcing 3.x, so cross-version drift is a real concern.

Ignore by identifier, never by message regex. Message-based ignoreErrors entries break the moment a rule is reworded (which happened across releases). Prefer identifier: scoping, and when a genuine false positive appears, add a code comment linking the upstream issue so it can be removed on the next upgrade.

Regenerate baselines after every extension upgrade. New rules (write-only and unused-property checks landed in 1.3.0) and reworded messages invalidate old baselines. Regenerate after inspecting the new findings — don't blindly accept the diff, or you'll baseline real bugs.

Child aggregates and nested entities. The initialization rule covers aggregate roots and child aggregates, but its heuristics may not model every domain-specific initialization path (state set by the parent, hydration hooks). Review those findings manually; if a false positive is genuine, prefer an inline @phpstan-ignore with a rationale over a global config ignore.

Avoid magic access to aggregate state. "Unused" and "write-only" checks rely on static read/write analysis. __get, reflection, or dynamic property names on aggregates will confuse both detection and suppression. Expose invariant checks via typed getters/methods so PHPStan sees the reads.

Stale result cache. If a finding you expect is missing: confirm includes:, run phpstan analyse --debug to confirm the file is scanned, check the class actually extends the library's aggregate base (the rule's type filter), and run phpstan clear-result-cache. Also check no ignoreErrors entry is silently suppressing it.

Non-standard vendor dir. If composer.json sets config.vendor-dir or you use a monorepo layout, the includes: path and the rule autoloading may not resolve. Use a relative path from the neon file to the actual install location.

Configurable rules — use them, don't disable them. Since 1.3.0 every rule is configurable. If the unused-property rule is too noisy on a specific legacy aggregate set, scope the rule's parameters to exclude those paths rather than turning the whole extension off for the app. That preserves coverage where it matters most.

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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
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
adachsoft/php-code-style