lychee-org/phpstan-lychee
PHPStan extension for Lychee that adds custom rules, stubs, and type information to improve static analysis of Lychee-based Laravel applications. It helps catch Lychee-specific errors earlier, enforce coding standards, and keep analysis consistent across projects.
This is a PHPStan ruleset, not an application package — the only thing it does for you is add custom rules to an existing PHPStan setup. Install it as a dev dependency and include its config file; there is nothing to boot, register in a service provider, or publish.
composer require --dev lychee-org/phpstan-lychee
First, look at what you actually got — the neon file name and the rule list are the whole API surface:
ls vendor/lychee-org/phpstan-lychee/
cat vendor/lychee-org/phpstan-lychee/*.neon
ls vendor/lychee-org/phpstan-lychee/src/Rules # rule classes + identifiers
Then include it from your project's phpstan.neon (or phpstan.neon.dist):
includes:
- vendor/lychee-org/phpstan-lychee/extension.neon
# likely also: rules.neon / config/extension.neon — use whatever ships
Run your normal analysis:
vendor/bin/phpstan analyse --memory-limit=1G
First use case: run it as a diff against your existing config before committing. Add the include on a branch, run PHPStan, and count how many new errors appear. If the count is large, you'll want a baseline segment (see below) rather than fixing everything at once. Check whether the rules are scoped to namespaces that exist in your repo — a Lychee-specific ruleset may assume Lychee\ namespaces and silently do nothing (or mis-fire) in a differently-namespaced project.
Separate the ruleset into its own include. Keep your base config clean so you can toggle the package on/off in CI or in a legacy subdirectory:
# phpstan-lychee.neon
includes:
- vendor/lychee-org/phpstan-lychee/extension.neon
# phpstan.neon
includes:
- phpstan-base.neon
- phpstan-lychee.neon # delete this line to opt out
Scope with paths per rule set. If only part of the codebase is expected to comply (new modules, app/Domain, etc.), give the strict config its own paths and run it as a second PHPStan invocation rather than bending the global rule level.
Use rule identifiers for surgical ignores. Custom rules ship an identifier (visible in the error output in parentheses, e.g. (Lychee.SomeRule), or readable from the getRuleIdentifier()/RuleErrorBuilder calls in the rule source). Ignore by identifier, never by message text — messages change between releases:
ignoreErrors:
- identifier: Lychee.SomeRule
path: src/Legacy/*
Wire into CI as a gate, with a baseline for the backlog. This is the pattern that actually sticks:
vendor/bin/phpstan analyse --configuration=phpstan.neon --error-format=github
Generate the initial baseline once, commit it, and treat "no new baseline entries" as the review rule:
vendor/bin/phpstan analyse --generate-baseline
Pair it with Larastan/larastan for Laravel repos. Load order matters: put your framework config first, then this package, so the Lychee rules see accurate types from the Laravel extension (model property types, facade return types, etc.). A custom rule that inspects an Eloquent model is only as useful as the type information reaching it.
Treat it as an architecture guardrail. The day-to-day value in a ruleset like this is usually enforcing a house convention — strict types declared, no env() outside config, enums instead of magic constants, controller/model boundaries. Those are the rules you want to fail early, so run PHPStan in a pre-push hook alongside Pint/PHP-CS-Fixer to keep feedback local:
"scripts": {
"lint:php": ["pint --test", "phpstan analyse --memory-limit=1G"]
}
Rule inclusion is the entire integration. There's no config/app.php entry and no service provider. If you see no new errors after including, the include path is almost certainly wrong, or the rules are namespace-scoped and your code doesn't match. Verify with phpstan analyse -v and confirm the extension is listed, and temporarily write a deliberate violation that a rule should catch.
Version-couple PHPStan deliberately. PHPStan's Rule interface and RuleErrorBuilder API have moved across major versions (notably the 1.x → 2.x line). With a ruleset this small and niche (1 star, 0 dependents), expect it to track a narrow PHPStan range. Pin both, and let Composer's conflict resolution — not hope — tell you if they disagree:
composer why-not phpstan/phpstan ^2.0
If you're on PHPStan 2.x and the package requires 1.x (or vice versa), you'll get rules that load but never fire, or a hard class-not-found. Don't fight it; check the package's own composer.json constraints first.
reportUnmatchedIgnoredErrors will bite you. When a custom rule is renamed or removed between releases, your identifier-scoped ignoreErrors entries become unmatched and the build fails. Keep the default (true) because that's the signal you want, but budget time for cleanup on every bump of this package — that's the real upgrade cost.
--generate-baseline can swallow your new rules. Generating a baseline after adding this package lumps every new error into phpstan-baseline.neon, where it's invisible in review. Generate the baseline first, then add the include, then look at the real diff. Otherwise you've paid for the ruleset and get zero signal.
Custom rules can't see what PHPStan can't infer. Most surprising "this rule never fires" bugs are typing problems, not rule bugs. Check with vendor/bin/phpstan analyse --debug or a temporary dumpType() — if a model property is mixed, the rule has nothing to reason about. Loading Larastan or adding @property docblocks usually fixes it.
Read the rule sources; that's the documentation. At this size there is unlikely to be thorough docs, and rule behavior is often configurable through neon parameters. Grep for parameters: in the shipped neon and for %...% interpolation in src/ to find tunables:
grep -rn "parameters:" vendor/lychee-org/phpstan-lychee/*.neon
grep -rn "%" vendor/lychee-org/phpstan-lychee/src --include=*.php | head -30
Fork-and-own is a legitimate strategy here. With a single-digit star count and no dependents, maintenance risk is the main concern. Because the extension surface is a handful of Rule classes plus a neon file, vendoring the rules into your own tools/phpstan/ directory (or a replace in composer.json) is cheap insurance and lets you adjust messages and thresholds to your codebase. Do that as soon as you find yourself writing ignoreErrors for the same identifier more than a couple of times — you're fighting the rule instead of using it.
Memory and parallelism. Custom rules that walk the AST over a large Laravel codebase push PHPStan past the default memory limit and can be slow. Set --memory-limit=1G (or higher) in CI and the composer script so failures are real rule violations, not OOM kills that look like timeouts.
How can I help you explore Laravel packages today?