Coding Standards
PHP-CS-Fixer applies the Symfony rule set mechanically, so formatting stops being a thing humans argue about in review.
vendor/bin/php-cs-fixer check --diff # same as: fix --dry-run --diffA coding standard is an agreed set of formatting rules: where braces go, how imports are sorted, whether arrays are written array() or []. None of it changes what the code does. All of it changes how quickly a diff can be read.
PHP-CS-Fixer applies a standard mechanically. @Symfony is the rule set the framework itself uses, and it extends @PER-CS, the community style that replaced PSR-12 as the reference. Writing '@Symfony' => true is shorthand for switching on every rule in that set individually; the fixer's rule reference lists the long form, and you override one rule by naming it after the set, which wins because later keys replace earlier ones.
Two commands, one analysis
fix rewrites files in place. check is shorthand for fix --dry-run: the same analysis, nothing written, and a non-zero exit code when a file would change. You run fix; CI runs check --diff so a failure shows what it wanted.
Configuration
The config file is PHP. It returns a PhpCsFixer\Config with rules and a PhpCsFixer\Finder that chooses files. Rules marked risky can change behaviour, not just layout, so a risky set does nothing until setRiskyAllowed(true) is set.
Why this belongs in CI
A reviewer spending a comment on a blank line is not spending it on the bug. Let the tool own formatting, and review correctness.
Example
// .php-cs-fixer.dist.php
declare(strict_types=1);
use PhpCsFixer\Config;
use PhpCsFixer\Finder;
$finder = (new Finder())
->in(__DIR__.'/src')
->in(__DIR__.'/tests')
// Generated files are rewritten by other tools; leave them alone.
->exclude('Migrations');
return (new Config())
->setRules([
// Shorthand for every rule in the Symfony set, which itself
// extends @PER-CS. The two lines below are the long form of
// two individual rules, applied after the set.
'@Symfony' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
])
->setFinder($finder);When to use it
- Make every pull request arrive already formatted, so review comments are about behaviour.
- Normalise a codebase that three developers wrote in three editors, in one commit.
- Fail a build when a file would change, without the build rewriting anything.
- Order imports and strip unused ones automatically instead of noticing them by eye.
More examples
Running it as a check in CI
This is the whole point of the tool: the standard is enforced by a machine that cannot be argued with and cannot be tired. Keep the job separate from tests, so a red build tells you at a glance whether it is style or substance.
# .github/workflows/quality.yaml
name: Quality
on: [push, pull_request]
jobs:
coding-standards:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
- run: composer install --no-interaction --no-progress
# check = fix --dry-run. Non-zero exit when a file would change.
# --diff prints the udiff so the log shows the expected result.
- run: vendor/bin/php-cs-fixer check --diffShort commands for the whole team
Composer puts vendor/bin on the PATH for scripts, so the binary needs no path prefix, and an @-prefixed entry calls another script. Add this once and nobody has to remember which flag means dry run. composer qa before pushing is then the same two commands CI will run.
{
"scripts": {
"cs": "php-cs-fixer fix",
"cs:check": "php-cs-fixer check --diff",
"sa": "phpstan analyse --no-progress",
"qa": [
"@cs:check",
"@sa"
]
}
}Opting in to rules that change behaviour
Use this when you want strict types or stricter comparisons applied across a codebase. Run it once on a branch of its own, read the diff properly, and run the test suite before merging: risky rules are the only ones that can turn a formatting commit into a behaviour commit.
// .php-cs-fixer.dist.php
declare(strict_types=1);
use PhpCsFixer\Config;
use PhpCsFixer\Finder;
$finder = (new Finder())->in(__DIR__.'/src');
return (new Config())
// Without this, every risky rule below is silently skipped.
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
// Risky sets are named with a :risky suffix.
'@Symfony:risky' => true,
// Adding strict_types to files that never had it can change
// behaviour, which is exactly why it counts as risky.
'declare_strict_types' => true,
])
->setFinder($finder);
Discussion