fabpot/php-cs-fixer
PHP CS Fixer automatically detects and fixes coding standards issues in your PHP code. It offers built-in rule sets like @PER-CS, @Symfony, and @PhpCsFixer, plus migration rules to modernise your codebase toward newer PHP and PHPUnit versions.
Install as a dev dependency (never in production):
composer require --dev friendsofphp/php-cs-fixer
# if dependency resolution conflicts with the app's packages:
composer require --dev php-cs-fixer/shim
Bootstrap a config and run it:
./vendor/bin/php-cs-fixer init
./vendor/bin/php-cs-fixer fix --dry-run --diff # preview
./vendor/bin/php-cs-fixer fix # apply
Where to look first
doc/ruleSets/index.rst — start from a set (@PER-CS, @Symfony, @PhpCsFixer, @auto) instead of hand-picking 60 rules.doc/rules/index.rst — the per-rule catalogue, with "risky" markers and examples../vendor/bin/php-cs-fixer describe <rule-or-set> — the fastest way to understand a rule's options and before/after code. This is the command you'll use daily.Important Laravel context: Laravel ships Pint, which is a thin opinionated wrapper around PHP CS Fixer. If the project already runs Pint, do not add a second, independently-configured PHP CS Fixer install — pick one. Reach for raw PHP CS Fixer when you need rule granularity, custom rules, or a rule set Pint doesn't expose; keep Pint when the team just wants "Laravel style, zero decisions".
First real use case: lock in a baseline on a fresh/legacy Laravel app — commit the config, run fix once in a dedicated commit ("style: apply php-cs-fixer"), then only ever run check in CI so diffs stay reviewable.
.php-cs-fixer.dist.php)<?php
use PhpCsFixer\Config;
use PhpCsFixer\Finder;
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
$finder = Finder::create()
->in([
__DIR__.'/app',
__DIR__.'/config',
__DIR__.'/database',
__DIR__.'/routes',
__DIR__.'/tests',
])
->exclude(['bootstrap/cache', 'storage'])
->notName('*.blade.php') // critical: never parse Blade
->notPath('_ide_helper.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return (new Config())
->setParallelConfig(ParallelConfigFactory::detect())
->setRiskyAllowed(false) // opt in deliberately, see Gotchas
->setRules([
'@PER-CS' => true,
// explicit overrides after the set:
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'single_quote' => true,
'trailing_comma_in_multiline' => [
'elements' => ['arrays', 'arguments', 'parameters'],
],
'yoda_style' => false, // team preference, avoids churn
])
->setFinder($finder)
->setCacheFile(__DIR__.'/.php-cs-fixer.cache');
Commit .php-cs-fixer.dist.php; keep .php-cs-fixer.cache in .gitignore.
"scripts": {
"lint": "php-cs-fixer check --diff",
"lint:fix": "php-cs-fixer fix"
}
composer lint should be the same command the CI job runs — one source of truth.
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
- run: composer install --no-interaction --prefer-dist
- run: vendor/bin/php-cs-fixer check --diff --format=github
Rely on the exit code (non-zero when changes are needed), not on parsing output. --format=github annotates the PR diff; checkstyle/junit/json exist for other CI systems. Cache the .php-cs-fixer.cache file keyed on PHP version + diff of the config to cut minutes off large repos.
#!/bin/sh
FILES=$(git diff --cached --name-only --diff-filter=ACM \
| grep -E '\.php$' | grep -v '\.blade\.php$')
[ -z "$FILES" ] && exit 0
vendor/bin/php-cs-fixer fix --path-mode=intersection $FILES || exit 1
git add $FILES
--path-mode=intersection is mandatory here: it intersects your passed paths with the configured Finder, so a stray vendor/ or storage/ path can't get rewritten.
./vendor/bin/php-cs-fixer fix app/Http/Controllers/OrderController.php./vendor/bin/php-cs-fixer fix --rules='{"@PER-CS":true,"yoda_style":false}' --dry-run --diff app/--verbose prints applied fixers per file; -vvv shows which rules produced each change — invaluable when a fix looks surprising.@autoPHPMigration / @autoPHPMigration:risky (and @autoPHPUnitMigration:risky for tests) rewrite syntax toward newer PHP/PHPUnit. Run these in their own commit, with the test suite green before and after.junstyle.vscode-php-cs-fixer. Point the editor at the same .php-cs-fixer.dist.php used by CI so format-on-save and the hook never disagree.sail exec -it laravel.test vendor/bin/php-cs-fixer fix; keep the cache file on a real filesystem mount, and consider hardcoding new ParallelConfig(4) instead of detect() in containers with many host CPUs exposed.Blade files will be corrupted. .blade.php is valid PHP-ish enough that the tokenizer may accept it, but directives and HTML get mangled. Always ->notName('*.blade.php'), or scope the Finder to app/, config/, database/, routes/, tests/.
Risky rules are silently inert. With setRiskyAllowed(false) (the default), rules like declare_strict_types, strict_comparison, void_return, mb_str_functions, psr_autoloading, no_unneeded_final_method are skipped without error. If a rule "does nothing", check the risky badge in describe. Then judge each one against Laravel reality: declare_strict_types can break implicit type juggling in request payloads and config values; strict_comparison will rewrite == you may have written intentionally. Prefer @PER-CS:risky piecemeal over wholesale.
Pint and PHP CS Fixer fight each other. Two configs with overlapping-but-different rules produce flip-flopping diffs (each tool reverts the other). Pick one runner; if you keep Pint, PHP CS Fixer is still installed as its transitive dependency, so never script both pint and php-cs-fixer.
Stale cache after config or version changes. The cache keys on rules + file hashes, but after a composer update of the fixer or a puzzling "no changes" result, run --using-cache=no or delete .php-cs-fixer.cache. In CI, either restore a correctly-keyed cache or disable it.
CLI order is not execution order. Fixers execute by internal priority, not by the order you list them in setRules(). Listing a rule after a set does override the set's options for that rule, but genuinely incompatible pairs (e.g. blank_line_after_opening_tag vs linebreak_after_opening_tag, or several phpdoc_* combinations) will warn/conflict — resolve by removing one, not by reordering.
Paths are resolved relative to the config file. Finder::create()->in([...]) with relative paths behaves differently depending on the CWD you invoke from. Use __DIR__ in the config and --config=.php-cs-fixer.dist.php when running from outside the project root.
PHP version support is a hard gate. Supported range moves over time (currently 7.4–8.5). Running on a newer PHP than supported fails fast; for local experiments only, use --allow-unsupported-php-version=yes or PHP_CS_FIXER_IGNORE_ENV=1. In CI, run the fixer on the same PHP minor as the app to avoid syntax the runtime can't parse.
Semantics-adjacent fixers to review in Laravel apps:
ordered_class_elements — large, noisy diffs; can interleave oddly with trait use blocks and constants. Introduce alone.binary_operator_spaces with align — causes perpetual churn on long arrays; use single_space if you want stable diffs.no_unused_imports / fully_qualified_strict_types — safe in v3 (annotations and attributes are understood), but confirm files that reference classes only inside strings (some service providers, phpstorm.meta.php) still resolve.header_comment — conflicts with blank_line_after_opening_tag if configured with a leading newline; set 'separate' => 'bottom' deliberately.Speed tips. Parallel mode is the single biggest win on big codebases; ParallelConfigFactory::detect() sizes to CPU count. For one-off checks, --dry-run still writes cache — combine --using-cache=no for throwaway experiments so you don't poison the cache with a temporary --rules set.
Debugging a wrong fix. Reproduce in isolation: php-cs-fixer fix --rules='{"the_suspect_rule":true}' --dry-run --diff path/to/file.php, then consult describe. If the rule is buggy, pin a version in composer.json and open an issue — the release notes show a steady stream of edge-case fixes (ClassDefinitionFixer, StatementIndentationFixer, PHP 8.4 property hooks), so upgrading promptly is usually the resolution.
Extension points. Custom fixers implement PhpCsFixer\Fixer\FixerInterface and register via $config->registerCustomFixers([new MyFixer()]); custom rule sets via RuleSet. This is genuinely useful for house rules (e.g. banning dd() in committed code, enforcing a company header) — but only consider it after exhausting the built-in catalogue, and cover custom fixers with the package's AbstractFixerTestCase so upgrades don't silently break them.
Don't self-update in a Composer install. That path is for the PHAR distribution; manage upgrades with composer update friendsofphp/php-cs-fixer --with-dependencies, then re-run check before merging to see the diff a version bump introduces.
How can I help you explore Laravel packages today?