phpcsstandards/phpcsextra
PHPCSExtra is a collection of extra sniffs and coding standards for PHP_CodeSniffer, including Modernize, NormalizedArrays, and Universal rules to help enforce consistent, modern PHP code. Requires PHP 5.4+ and PHPCS 3.13.6/4.x.
PHPCSExtra is not a formatter you run on its own — it is a set of extra sniffs that plug into PHP_CodeSniffer and get composed into your ruleset. Your first job is not "pick a standard", it's "decide which sniffs belong in this Laravel app".
1. Install (dev only). The allow-plugins step must come before require, otherwise Composer 2.2+ blocks the installer plugin and the standards never register with PHPCS:
composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev phpcsstandards/phpcsextra
Do not add squizlabs/php_codesniffer or phpcsstandards/phpcsutils to your own composer.json. PHPCSExtra (and any other external standard you install) manages those version constraints transitively — pinning them yourself is how you end up in --with-all-dependencies dependency-hell six months later. If they are already in your require-dev, remove them.
2. Verify registration — this is always your first debugging step:
vendor/bin/phpcs -i
You should see Modernize, NormalizedArrays, Universal appended to the list. If they're missing, the plugin didn't register: check allow-plugins, then composer dump-autoload.
3. Look at what you actually installed before writing a ruleset:
vendor/bin/phpcs -e --standard=Modernize # list sniffs in a standard
vendor/bin/phpcs -e --standard=Universal # ~40 sniffs, read this list
vendor/bin/phpcs --generator=Text --standard=NormalizedArrays # read the docs
Most sniffs ship embedded documentation (:books: in the README) and many ship metrics (:bar_chart:, viewable with --report=info). Read the docs from the CLI — it's faster than the GitHub README and matches the exact version you installed.
4. First use case — audit config/ and lang/, not app/. This is the lowest-risk, highest-signal entry point in a Laravel codebase. Laravel config files are giant hand-merged arrays, which is exactly what Universal.Arrays.DuplicateArrayKey, Universal.Arrays.MixedArrayKeyTypes and Universal.Arrays.MixedKeyedUnkeyedArray are built for. These are analysis-only sniffs (no fixer), so you get findings without any risk of a tool rewriting 200 lines of config:
vendor/bin/phpcs --standard=Universal.Arrays.DuplicateArrayKey \
--extensions=php --report=full config/ lang/
A duplicate key in config/services.php silently clobbers the earlier value and produces a mystery "why is my API key empty" bug — this sniff earns its keep on day one.
Universal is explicitly not a standard — it's a sniff collection containing mutually contradictory rules (DisallowFinalClass vs RequireFinalClass, DisallowAnonClassParentheses vs RequireAnonClassParentheses, DisallowAttributeParentheses vs RequireAttributeParentheses). Including <rule ref="Universal"/> in a ruleset is the single most common mistake with this package. Modernize and NormalizedArrays are coherent standards and can be adopted as blocks.
A phpcs.xml.dist that a Laravel dev can actually live with:
<?xml version="1.0"?>
<ruleset name="Laravel App">
<description>Analysis sniffs from PHPCSExtra; formatting handled by Pint.</description>
<file>app</file>
<file>config</file>
<file>database</file>
<file>routes</file>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/storage/*</exclude-pattern>
<exclude-pattern>*/bootstrap/cache/*</exclude-pattern>
<exclude-pattern>*/node_modules/*</exclude-pattern>
<arg name="extensions" value="php"/>
<arg name="colors"/>
<arg name="cache" value=".phpcs.cache"/>
<config name="php_version" value="80200"/>
<rule ref="Universal.Arrays.DuplicateArrayKey"/>
<rule ref="Universal.Arrays.MixedArrayKeyTypes"/>
<rule ref="Universal.Arrays.MixedKeyedUnkeyedArray"/>
<rule ref="Universal.CodeAnalysis.ForeachUniqueAssignment"/>
<rule ref="Universal.CodeAnalysis.NoDoubleNegative"/>
<rule ref="Universal.CodeAnalysis.NoEchoSprintf"/>
<rule ref="Universal.CodeAnalysis.ConstructorDestructorReturn"/>
</ruleset>
Then wire it into Composer so nobody has to remember the invocation:
"scripts": {
"lint": "phpcs --standard=phpcs.xml.dist",
"lint:fix": "phpcbf --standard=phpcs.xml.dist"
}
Pint is PHP-CS-Fixer; PHPCSExtra is PHP_CodeSniffer. They are unrelated engines that will both try to own the same whitespace. The stable arrangement is a split of responsibilities:
NormalizedArrays.Arrays.ArrayBraceSpacing, NormalizedArrays.Arrays.CommaAfterLast, or the Attributes.* spacing/parentheses sniffs while Pint is the formatter — you'll get two tools fighting over the same lines, phpcbf rewriting what pint just wrote, and a diff that flip-flops forever in CI.CodeAnalysis.* and Arrays.Duplicate* / Arrays.Mixed* sniffs are exactly the class of checks Pint cannot do, which makes this a genuinely complementary install rather than a duplicate one.If you're dropping Pint in favour of PHPCS, then adopt NormalizedArrays as a block — its defaults (CommaAfterLast: forbid on single-line, enforce on multi-line) match modern Laravel style closely and phpcbf will handle migration in one pass. Just remember to pair it with an indentation sniff; ArrayBraceSpacing explicitly does not handle indentation even when set to newline.
php_version config is mandatory, not optionalSeveral sniffs branch on the target PHP version: Modernize.FunctionCalls.Dirname (won't suggest __DIR__/$levels above the configured version), Universal.Arrays.DuplicateArrayKey (negative numeric keys behave differently pre-8.0 and post-8.0), Universal.CodeAnalysis.ConstructorDestructorReturn (PHP4-style constructors only flagged below 8.0). Set it once in the ruleset, or per-run:
vendor/bin/phpcs --runtime-set php_version 80200 --standard=phpcs.xml.dist
Leave it unset and the sniffs infer from the host PHP, which means your CI result changes when the runner image bumps a minor version. That's a maddening class of flake.
For a codebase inherited from PHP 5.x/7.x, don't fold Modernize into the main ruleset — run it as a one-shot, reviewable pass:
git switch -c lint/modernize
vendor/bin/phpcbf --standard=Modernize --extensions=php app/ database/ routes/
git diff --stat && git diff
Modernize.FunctionCalls.Dirname is the whole content here (dirname(__FILE__) → __DIR__, nested dirname() → $levels). On a fresh Laravel app it will find almost nothing — its value is entirely in migration work, and it's the safest fixer in the package.
Every sniff exposes error codes, and many are modular specifically so you can disable one behaviour without losing the rest. Get the source code with -s, then exclude just that:
vendor/bin/phpcs -s --standard=phpcs.xml.dist app/
<!-- static is fine here; we only care about instanceof/new -->
<rule ref="Universal.CodeAnalysis.StaticInFinalClass">
<exclude name="Universal.CodeAnalysis.StaticInFinalClass.ReturnType"/>
</rule>
Same trick for Universal.CodeAnalysis.NoDoubleNegative.FoundDoubleWithInstanceof (not auto-fixable) and Universal.CodeAnalysis.NoDoubleNegative.FoundTriple. Use inline // phpcs:ignore Universal.Arrays.DuplicateArrayKey -- intentional override for deliberate cases; phpcs:disable/phpcs:enable around generated blocks.
Universal.Arrays.DisallowShortArraySyntax in a Laravel project. Laravel style is short arrays everywhere; the sniff's existence is for legacy codebases migrating toward []. (It is, however, the correct choice over the PHPCS-native Generic.Arrays.DisallowShortArraySyntax in those legacy repos specifically because it ignores short list syntax and won't corrupt [$a, $b] = $x when the fixer runs.)Universal.Classes.RequireFinalClass is a flagged risky fixer — treat the fixer as off-limits. It will mark your Eloquent models and service classes final, which breaks Mockery/PHPUnit mocking, and breaks any consumer that extends them. If you want the sniff, run it as an error-only advisory and exclude tests/ and app/Models/, and never let phpcbf touch it. readonly classes are safer territory, but the same mocking caveat applies.Universal.Classes.RequireFinalClass + Universal.CodeAnalysis.StaticInFinalClass are a package deal. The latter only reports static → self inside classes that are actually final. Enabling it before you've adopted finality produces zero findings and looks broken.phpcbf is aggressive. Always run it on a clean working tree with the ruleset committed, so git diff is your review surface. For the whole-repo migration case, run phpcbf, inspect, then phpcs again — some sniff/fixer pairs need two passes to settle.composer update needs --with-all-dependencies if you have leftovers. If squizlabs/php_codesniffer or phpcsstandards/phpcsutils are still pinned in your composer.json, a plain composer update phpcsstandards/phpcsextra --with-dependencies will refuse to move them. Remove them from your manifest instead of escalating the flag — that's the actual fix.--cache=.phpcs.cache makes iterative runs 5–10x faster on a mid-size app, but the cache doesn't always invalidate when you change the ruleset, only when files change. If a sniff you just enabled reports nothing, delete .phpcs.cache before you start debugging the sniff. Add it to .gitignore.phpcs -i vs phpcs -e. -i proves registration; -e proves the sniff name string you typed is real. Typo'd sniff names in a ruleset are silently ignored by PHPCS — no error, no warning, just a rule that never fires. When a rule "isn't working", verify with -e first.Universal.Arrays.DuplicateArrayKey and the Mixed* sniffs do real token work on big array literals. On generated files (translation dumps, huge config/*.php), scope the ruleset with <file> entries per directory rather than pointing it at the project root. Split slow CI runs by directory (app/, database/, config/) if a single invocation times out.--report=info is underused. The :bar_chart: sniffs produce metrics — useful when you're deciding whether to adopt a rule ("how many single-line arrays have trailing commas today?") rather than guessing.<exclude-pattern> for vendor/, storage/, bootstrap/cache/, node_modules/, and any *.blade.php-adjacent PHP. PHPCS walking vendor/ is the usual reason a first run takes 90 seconds.phpcs.xml.dist <rule ref="path/to/MySniff"/>) and compose PHPCSExtra underneath it — never fork the package. It's a Composer dependency; local patches get blown away on the next composer update.E_DEPRECATED or throw. If you're pinned to an old PHPCS for another standard's sake, resolve that first — PHPCSExtra is not the package to downgrade around.How can I help you explore Laravel packages today?