irstea/php-cs-fixer-shim
A compatibility shim for php-cs-fixer that provides a stable entry point for PHP coding-style checks and fixes, helping projects avoid dependency and version conflicts while keeping formatting tooling consistent.
A "shim repository" for friendsofphp/php-cs-fixer means this package exists to provide php-cs-fixer, not to replace its behaviour. Day to day, you run php-cs-fixer exactly as you always have; the shim's job is install/version plumbing (private GitLab, dependency pinning, or working around a constraint the upstream package can't satisfy in your environment).
composer.json first. Everything you need to know is there: which friendsofphp/php-cs-fixer version range it requires, whether it exposes a bin, and whether it declares replace/provide. This is the file that tells you what you're actually installing.composer config repositories.irstea vcs https://gitlab.irstea.fr/pole-is/tools/php-cs-fixer-shim.git
composer require --dev irstea/php-cs-fixer-shim
If GitLab requires auth, configure a token once: composer config --global gitlab-token.gitlab.irstea.fr <token>.vendor/bin/php-cs-fixer --version. If the shim doesn't provide bin, the binary comes from the underlying dependency — check both.vendor/bin/php-cs-fixer fix src --dry-run --diff
.php-cs-fixer.php at the project root:
<?php
$finder = PhpCsFixer\Finder::create()
->in(__DIR__.'/src')
->in(__DIR__.'/tests')
->exclude('var');
return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@PSR12' => true,
'@PSR12:risky' => true,
'declare_strict_types' => true,
'ordered_imports' => true,
])
->setFinder($finder);
Two modes, one config. Locally you fix; in CI you check. Keep a single .php-cs-fixer.php and drive the mode from the command:
# developer machine
vendor/bin/php-cs-fixer fix
# CI — fails the build on any violation
vendor/bin/php-cs-fixer fix --dry-run --diff
Composer scripts as the interface. Nobody should have to remember flags. Wrap them:
"scripts": {
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix"
}
Then composer cs:check is the one command contributors learn.
GitLab CI. This is the natural home for a shim hosted on GitLab:
coding-style:
image: php:8.2-cli
script:
- composer install --no-progress --prefer-dist
- composer cs:check
Because the shim pins the fixer version, the CI job and every developer machine stay in lockstep — that's the main payoff of using it.
Editor integration. Point PhpStorm's "File Watcher" or VS Code's php-cs-fixer extension at vendor/bin/php-cs-fixer and the project config file. No extra config needed beyond the path.
Pre-commit hook. Fix only what changed and let the rest of the tree be — the config's Finder still governs the run:
vendor/bin/php-cs-fixer fix --path-mode=intersection $(git diff --cached --name-only --diff-filter=ACM)
Or delegate to GrumPHP/CaptainHook if the project already uses them.
Rule sets, layered. Start with @PSR12, then @Symfony or @PhpCsFixer for stricter projects, then sprinkle individual fixers. Add @PHP82Migration / @PHP83Migration when bumping language versions — it handles the mechanical rewrites for you.
Custom fixers. Register them once and they become part of the same run:
->registerCustomFixers([new App\Fixer\NoDebugDump())
->setRules([...'App/no_debug_dump' => true])
The shim controls the version — know what you're pinned to. Because it requires a specific friendsofphp/php-cs-fixer range, an upgrade of the shim can silently bump the fixer and produce a large reformatting diff. Check the shim's composer.json before running composer update, and commit formatting changes separately from version bumps.
Private repo auth fails silently in CI. If the composer install step can't reach gitlab.irstea.fr, you get a confusing "binary not found" at lint time. Set COMPOSER_AUTH or use a deploy token in .gitlab-ci.yml before anything else.
Always gitignore .php-cs-fixer.cache. The fixer writes a cache file at the project root by default; committing it causes pointless diffs and cross-machine churn.
Risky rules are opt-in. @PSR12:risky, declare_strict_types, strict_param etc. silently no-op unless the config has ->setRiskyAllowed(true) and you don't pass --allow-risky incorrectly. If a rule "doesn't work", this is the first thing to check.
Rule order matters. Fixers run in a fixed internal order, but overlapping rules (e.g. ordered_class_elements alongside a custom class-body fixer) can fight. Narrow the rule set on the conflicting path rather than disabling one globally.
--diff-format is gone in v3. Old CI snippets that pass --diff-format=udiff will error. Just use --diff.
Exit codes for CI. --dry-run alone exits non-zero on violations; add --using-cache=no if you want CI to never trust a cache carried in the workspace. Avoid --stop-on-violation unless you're debugging which file breaks first — it hides the rest of the report.
Run the same PHP binary as production. php-cs-fixer's output depends on the PHP version it runs under (syntax-level fixers like @PHP81Migration). If CI uses php:8.2-cli and your laptop runs 8.4, fixers can disagree. Pin the image, or run php-cs-fixer via the project's PHP binary explicitly.
Debugging a stubborn file:
vendor/bin/php-cs-fixer fix path/to/File.php --dry-run --diff -vvv --show-progress=none
-vvv prints the applied rules; if a rule you expect isn't listed, it's either not enabled, suppressed by setRiskyAllowed, or the Finder isn't reaching that file — check Finder in()/exclude() next.
If the shim goes stale. The clean escape hatch is to remove it and require friendsofphp/php-cs-fixer directly with an explicit constraint; nothing in your .php-cs-fixer.php or CI changes. Before doing that, diff the shim's composer.json against upstream to see what it was actually pinning — sometimes it exists to keep a transitive dependency (e.g. symfony/*) in a compatible range, and that constraint needs to survive the swap.
How can I help you explore Laravel packages today?