etsglobal/php-static-analysis
A PHP static analysis package from ETS Global for inspecting PHP codebases and supporting code quality workflows. Integrates into Laravel/PHP projects to help surface potential issues, encourage cleaner code, and complement existing test and CI pipelines with additional static checks.
This is a configuration-distribution package, not an analysis engine. It ships opinionated config files (PHPStan/Psalm/PHP-CS-Fixer/Rector-style) plus the dev dependencies they need, so a new project gets a working static-analysis stack in one composer require --dev instead of hand-tuning four tool configs. Treat it as a starter kit you own and fork, not a black box you depend on.
Minimal steps:
composer require --dev etsglobal/php-static-analysiscomposer.json first — that's the real entry point. Look at:
require-dev block: which tools and which versions it pins.scripts it registers (typically analyse, lint, fix, cs, rector).extra block, if configs are published/copied rather than included.phpstan.neon.dist, .php-cs-fixer.dist.php, psalm.xml, rector.php — whichever exist). These are your extension points.composer analyse
composer lint
include (live, upgraded with the package) or published via a vendor:publish-style copy (frozen at install time). This single fact determines your whole upgrade strategy.First use case: drop it into a mid-size existing Laravel app, run the scripts, and immediately generate a baseline for pre-existing violations (see below). That gets you CI enforcement on new code on day one without a week of cleanup.
If the package is thin or stale (0 stars, 0 dependents — verify before betting on it), the pragmatic move is to vendor the config files into your own repo as .phpstan.neon.dist etc. and drop the dependency. You keep the opinions, you lose the surprise upgrades.
Composer scripts as the single interface. Never let developers invoke vendor/bin/phpstan directly with ad-hoc flags — you'll get divergent results between local and CI. Everything goes through the script names. If the package doesn't define convenient ones, add them in your composer.json:
"scripts": {
"analyse": "phpstan analyse --memory-limit=1G --no-progress",
"analyse:baseline": "phpstan analyse --generate-baseline",
"lint": "php-cs-fixer fix --dry-run --diff",
"fix": "php-cs-fixer fix"
}
Extend, don't edit. In phpstan.neon, include the vendor config and layer your project specifics on top:
includes:
- vendor/etsglobal/php-static-analysis/phpstan.neon
- phpstan-baseline.neon
parameters:
paths:
- app
- src
excludePaths:
- app/Console/Kernel.php
Same shape for PHP-CS-Fixer ($config->setRules([...]) after the imported defaults) and Rector. The layering order matters — later includes and local parameters win.
Baselines for legacy debt. The single highest-leverage pattern with a package like this:
phpstan-baseline.neon (or Psalm's --set-baseline).Without a baseline, teams install this package, see 4,000 errors, and uninstall it within a week.
CI integration, tiered. Two jobs, not one:
--dry-run --diff mode only. Seconds, no cache needed, catches 80% of style review comments.cacheKey keyed on composer.lock hash). Cache restore is the difference between a 40-second and 6-minute CI job.Pre-commit hook as a courtesy, CI as the gate. Wire composer lint and a scoped composer analyse into a Husky/CaptainHook pre-commit, but keep CI authoritative. Local hooks get bypassed with --no-verify under deadline pressure; CI doesn't.
Level ratcheting. If the package ships PHPStan at a conservative level (5 or 6), pin that, then bump one level per quarter as the baseline shrinks. Going to level max on a Laravel app on day one is a non-starter because of magic (__get, facades, Eloquent dynamic properties).
Framework glue. For Laravel you will almost certainly need one of larastan/laravel-phpstan-style extensions on top of this package's base config. Check whether the package already pulls one in before adding your own — double-registering the same extension produces confusing duplicate-error output.
Config merge semantics will bite you. NEON includes merge parameters but replace scalar keys and append to arrays in ways that aren't always intuitive; PHP-CS-Fixer's setRules with an imported config can silently drop defaults if you reassign rather than array_merge. After any config change, prove the effective rules with the tool's own debug output:
vendor/bin/phpstan analyse -c phpstan.neon --debug # see actual config resolution
vendor/bin/php-cs-fixer describe <rule> # confirm a rule is active
Version pinning is the main supply-chain risk. A config package's opinions are only valid against the tool versions it was written for. If it pins phpstan/phpstan: ^1.x and you need 2.x, you get config-schema breakage (parameters renamed, ignoreErrors format changed in 1.x→2.x). Lock the package to an exact minor and upgrade deliberately, reading its changelog — for a 0-star package, assume the changelog is thin or absent, so diff the config files on upgrade instead.
Don't analyze vendor/. Obvious, but config packages frequently ship broad paths or miss excludePaths. Memory blows up and you get thousands of third-party errors. Verify excludePaths covers vendor, storage, bootstrap/cache, and generated code (migrations you don't own, IDE helper files like _ide_helper.php).
Memory and parallelism. Allowed memory size exhausted is the most common first-run failure. --memory-limit=1G in the script is the fix; don't ask developers to pass it manually. On CI, --configuration plus parallel workers helps, but parallel mode can mask ordering-dependent issues in custom rules — if results differ between -j1 and parallel runs, that's a bug in a rule, not your config.
Baseline drift causes phantom CI failures. Baselines are keyed on file path + message + count. Renaming a file, changing an error's line, or fixing one instance can invalidate entries and make CI fail for a change that improved things. Regenerate the baseline in the same PR as any refactor that touches baselined files, and review the baseline diff like you'd review code.
Autoload mismatches masquerade as analysis bugs. "Class not found" during analysis usually means an optimized autoloader hasn't been dumped (composer dump-autoload -o) or the class lives in dev autoload scope. Always run dump-autoload in CI before analysis, and prefer composer install over copying a cached vendor/ between jobs.
PHP version matrix. If the package's config assumes a phpVersion (or php_version parameter) and your runtime differs, you'll get false positives on syntax/features. Set it explicitly in your local layer to match composer.json's require.php, and keep it in sync — it's a silent source of "why is PHPStan complaining about readonly?"
Extension points to know. For PHPStan: custom rules registered via rules: and error suppression via ignoreErrors: with identifier (prefer identifiers over message regex — message text changes across versions and regexes rot). For PHP-CS-Fixer: custom Finder excludes and a setRiskyAllowed(true) decision — risky fixers change semantics, so opt in per-rule, not globally.
Debugging a suspicious error. Reproduce with -v/--debug to see which rule or extension produced it, then check whether the package or a framework extension is responsible before writing a suppression. ignoreErrors should always carry a comment explaining why — unexplained suppressions are how static analysis quietly dies.
Bottom line for adoption: this package's value is entirely in the quality and freshness of its bundled config. With 0 dependents and no stars, budget an hour to read the configs before committing — you're evaluating borrowed opinions, and you may well end up vendoring them into your own repo, which is a perfectly good outcome.
How can I help you explore Laravel packages today?