squizlabs/php_codesniffer
PHP_CodeSniffer tokenizes PHP files to detect coding standard violations and automatically fix them. It ships two scripts: phpcs for sniffing and phpcbf for auto-correction, keeping code clean and consistent across teams and CI. The official continuation of the Squizlabs package.
1. Install as a dev dependency (never production).
composer require --dev squizlabs/php_codesniffer:^4.0
On older projects pinned to PHP 7.x/early 8.x, ^3.13 is the safer line; the 4.x major changed default behaviour and some sniff messages.
2. Run it once, on a narrow path, before writing any config.
./vendor/bin/phpcs app/Http/Controllers --standard=PSR12 -s
-s prints the sniff source code (e.g. PSR12.Files.FileHeader.SpacingAfterBlock) — this is the single most useful flag, because that string is what you paste into phpcs:ignore annotations and <exclude name="..."> rules.
3. Where to look first. The README is thin; the real documentation is the GitHub wiki: Usage, Advanced Usage (default configuration file), Configuration Options, and Annotated Ruleset. Read Annotated Ruleset before writing your own phpcs.xml.dist, not after.
4. First use case: a project ruleset file. Drop this at the repo root as phpcs.xml.dist and both phpcs and phpcbf pick it up automatically with no arguments:
<?xml version="1.0"?>
<ruleset name="App">
<description>Project coding standard.</description>
<file>app</file>
<file>config</file>
<file>database</file>
<file>routes</file>
<file>tests</file>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/node_modules/*</exclude-pattern>
<exclude-pattern>*/storage/*</exclude-pattern>
<exclude-pattern>*/bootstrap/cache/*</exclude-pattern>
<arg name="extensions" value="php"/>
<arg name="colors"/>
<arg name="parallel" value="8"/>
<arg name="cache" value=".phpcs.cache"/>
<arg value="ps"/>
<rule ref="PSR12"/>
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="120"/>
<property name="absoluteLineLimit" value="0"/>
</properties>
</rule>
</ruleset>
Then ./vendor/bin/phpcs and ./vendor/bin/phpcbf are your two everyday commands.
Composer scripts as the team interface. Nobody should have to remember flags:
"scripts": {
"lint": "phpcs",
"lint:fix": "phpcbf",
"lint:ci": "phpcs -q --report=checkstyle --report-file=build/phpcs.xml"
}
The fix-then-verify loop. phpcbf runs a limited number of fixer passes, and some sniffs only become fixable after another sniff has fixed the spacing around them. The reliable workflow is:
./vendor/bin/phpcbf app/Http/Controllers/FooController.php
./vendor/bin/phpcbf app/Http/Controllers/FooController.php # second pass catches leftovers
./vendor/bin/phpcs app/Http/Controllers/FooController.php # confirm clean
git diff # ALWAYS eyeball the diff
Commit (or stash) before running phpcbf on a large tree — the fixer is powerful and occasionally wrong on unusual syntax.
CI gate. Use an explicit path rather than . so vendor/ never gets scanned:
- run: composer install --prefer-dist --no-progress
- run: ./vendor/bin/phpcs --report=checkstyle --report-file=checkstyle.xml
Exit codes matter: 0 clean, 1 violations found, 2 fixer failed, 3 processing/parse error. Treat 3 as a hard failure — it usually means an unparseable file or a tokenizer problem, not a style issue.
Pre-commit hook. Scope it to staged files only, or developers will disable it:
git diff --cached --name-only --diff-filter=ACM -- '*.php' \
| xargs -r ./vendor/bin/phpcs --standard=phpcs.xml.dist
GrumPHP, CaptainHook, or a Husky lint-staged entry all wrap this fine.
Baseline for legacy codebases. Instead of <exclude-pattern>-ing half the app, freeze existing debt:
./vendor/bin/phpcs --generate-baseline # writes phpcs-baseline.xml
./vendor/bin/phpcs --baseline=phpcs-baseline.xml
New violations fail CI; old ones are ignored until touched. Commit the baseline file and shrink it over time rather than regenerating it.
Enforce Laravel-specific conventions with built-in sniffs. You can get most of the way without writing a custom sniff:
<!-- no debug helpers in committed code -->
<rule ref="Generic.PHP.ForbiddenFunctions">
<properties>
<property name="forbiddenFunctions" type="array">
<element key="dd" value="null"/>
<element key="dump" value="null"/>
<element key="var_dump" value="null"/>
<element key="ray" value="null"/>
</property>
</properties>
</rule>
<!-- env() only belongs in config/ -->
<rule ref="Generic.PHP.ForbiddenFunctions">
<properties>
<property name="forbiddenFunctions" type="array">
<element key="env" value="null"/>
</property>
</properties>
<exclude-pattern>config/*</exclude-pattern>
</rule>
Custom sniffs for house rules. When a rule can't be expressed declaratively (e.g. "controllers must not exceed 300 lines", "no DB::raw() with an interpolated variable"), write a sniff. The pattern is always the same: extend PHP_CodeSniffer\Sniffs\Sniff, implement register() returning token constants, and process() emitting $phpcsFile->addError() / addWarning() / addFixableError(). Register it in the ruleset with <rule ref="AppStandard.Laravel.NoDbRaw"/> and point installed_paths at the directory containing AppStandard/:
./vendor/bin/phpcs --config-set installed_paths /path/to/your/standards
If you distribute standards via Composer, install phpcsstandards/composer-installer (the maintained successor to dealerdirect/phpcodesniffer-composer-installer) so installed_paths is wired up automatically.
Reports tailored to the audience. --report=summary for a quick file-level overview, --report=source to see which sniff is causing most of the noise (useful when deciding what to exclude), --report=diff to preview what phpcbf would change without touching files, --report=json for scripting.
Complement, don't replace, static analysis. PHPCS is token-level style and convention enforcement. It will not tell you a method returns the wrong type or that a query is N+1. Pair it with Larastan/PHPStan; they overlap almost not at all.
vendor/ for you. Running phpcs . from the project root will crawl vendor/, node_modules/, and storage/. Always declare <file> entries explicitly or add <exclude-pattern> rules. This is the number-one cause of "PHPCS is so slow".--standard or ship a ruleset.phpcs.xml beats phpcs.xml.dist. The .dist file is the committed one; a developer's local phpcs.xml overrides it silently. Add phpcs.xml and .phpcs.cache to .gitignore and tell the team why.--standard=/absolute/path/phpcs.xml.dist from a different directory still resolves <file> correctly, which is what you want in CI..blade.php files are mostly inline HTML to the tokenizer; you'll get noise from Generic.Files.LineLength and little else of value. Exclude resources/views and lint the compiled/cached output only if you must.phpcs:ignore placement is strict. It applies to the next line, or to the same line if written at the end of it. // phpcs:ignore Sniff.Name above a multi-line statement only suppresses the first line's errors. Prefer phpcs:disable/phpcs:enable for blocks, and always include the sniff name — bare ignores rot.phpcbf may oscillate or emit a "fixer conflict" notice and stop touching the loop. Fix by excluding one sniff for that path (<exclude name="..."/> inside an <exclude-pattern> block) rather than fighting it.--parallel speeds up big trees but scrambles report ordering and can break custom sniffs with static state. On large repos also set php -d memory_limit=512M vendor/bin/phpcs..phpcs.cache keys off file mtime/size; stale results after a git checkout or branch switch are not unheard of. --no-cache when debugging a phantom violation.Generic.Files.LineLength defaults (80/100) are far stricter than Laravel's ~120. Set lineLimit to 120 and absoluteLineLimit to 0 to disable the hard cap, or you'll get a wall of errors on day one and the team will tune the tool out.--runtime-set key value for one-off property overrides without editing the ruleset, <config name="..." value="..."/> for global settings, <rule ref="..."><exclude name="Sniff.Code"/></rule> for surgical disabling, and phpcs -e to list every installed sniff and phpcs -i to list installed standards — the fastest way to confirm a custom standard is actually registered.How can I help you explore Laravel packages today?