codeigniter/coding-standard
Official coding standard for CodeIgniter 4, built on PHP_CodeSniffer. It enforces consistent PHP code style and quality across CodeIgniter projects, with rules aligned to the framework's conventions and easy integration into Composer workflows.
This is not a runtime library — there is no service provider, no publishable config, no Artisan command. It is a PHP CS Fixer ruleset (two of them: CodeIgniter4 and CodeIgniter4Risky) that you point a .php-cs-fixer.dist.php file at. For a Laravel developer the day-to-day value is either (a) a second opinionated preset to evaluate against Pint, or (b) the canonical style for code that must also live in a CodeIgniter 4 package.
1. Install as a dev dependency
composer require --dev codeigniter/coding-standard
This pulls in php-cs-fixer and nexusphp/cs-config; you get vendor/bin/php-cs-fixer for free.
2. Create a config at the repo root
<?php
// .php-cs-fixer.dist.php
use CodeIgniter\CodingStandard\CodeIgniter4;
use Nexus\CsConfig\Factory;
use PhpCsFixer\Finder;
$finder = Finder::create()
->files()
->in(__DIR__)
->exclude(['vendor', 'node_modules', 'storage', 'bootstrap/cache', 'public'])
->notName('*.blade.php') // critical — see Gotchas
->notName('_ide_helper*.php')
->notPath('config/*.cache.php');
return Factory::create(new CodeIgniter4(), ['finder' => $finder])
->forProjects();
Swap new CodeIgniter4() for new CodeIgniter4Risky() if you want the aggressive set (it enables risky fixers such as declare_strict_types).
3. Look first at the diff, not the fix
Never run fix on a real branch before you have read the report:
vendor/bin/php-cs-fixer fix --dry-run --diff --using-cache=no src/
--diff shows the exact hunks per file; --using-cache=no guarantees you are seeing the true state rather than a stale cache. Start with one directory (app/Http, src/) so the output is reviewable.
4. Wire up composer scripts
"scripts": {
"cs": "php-cs-fixer fix",
"cs:check": "php-cs-fixer fix --dry-run --diff --using-cache=no"
}
First use case: run composer cs:check against a single leaf directory of an existing Laravel app — typically app/Support or app/Domain — read the diff, and decide whether the preset is a wholesale replacement for Pint or a source of rules to cherry-pick.
Pattern A — Adopt wholesale, retire Pint. Remove laravel/pint and its pint.json, keep only .php-cs-fixer.dist.php, and repoint every editor/CI hook at vendor/bin/php-cs-fixer. Cleanest if you actually like the CI4 ordering rules (ordered_class_elements, ordered_imports, PHP-Doc normalisation) — these are stricter than the Laravel preset and produce a noticeably more uniformly sorted class body.
Pattern B — Cherry-pick into pint.json. Pint is a PHP CS Fixer wrapper, so PHP CS Fixer fixer names work directly under rules. Read what the ruleset enables and copy only what you want:
# enumerate the rules and their config for a given ruleset
php -r 'require "vendor/autoload.php";
$r = (new CodeIgniter\CodingStandard\CodeIgniter4())->getRules();
foreach ($r as $k => $v) printf("%-45s %s\n", $k, json_encode($v));'
Then in pint.json:
{
"preset": "laravel",
"rules": {
"ordered_class_elements": { "order": ["use_trait", "constant_public", "property_public", "construct", "method_public"] },
"phpdoc_order": true,
"phpdoc_separation": true,
"no_superfluous_phpdoc_tags": false
}
}
This is the highest-leverage pattern for a Laravel team: you keep Pint's ergonomics and Laravel preset while stealing the CI4 rules you actually miss.
Pattern C — Dual-target package (CI4 + Laravel in one repo). Keep two config files and two scripts so each subtree gets its native style, and let CI run both:
// .php-cs-fixer.ci4.php → fixes src/CodeIgniter/ only, ruleset CodeIgniter4
// pint.json → fixes everything else (Laravel preset)
"scripts": {
"cs:ci4": "php-cs-fixer fix --config=.php-cs-fixer.ci4.php",
"cs:app": "pint"
}
Use --path-mode=intersection when you pass explicit paths so the CLI arguments are still filtered by the Finder's excludes rather than overriding them.
Pattern D — CI gate with a dedicated, cache-less check. In GitHub Actions (or any CI), run the check step without a cache and with a machine-readable formatter so failures annotate the PR:
- run: composer install --no-interaction --prefer-dist
- run: vendor/bin/php-cs-fixer fix --dry-run --diff --using-cache=no --format=checkstyle
Keep the write-mode run (composer cs) local only — never let CI push formatted commits back to the branch, or you get surprise rebases.
Pattern E — Pre-commit on changed files only. Full-repo runs get slow past a few thousand files. Restrict to staged PHP:
vendor/bin/php-cs-fixer fix --path-mode=intersection $(git diff --cached --name-only --diff-filter=ACM | grep -E '\.php$')
Hook this into captainhook, Husky, or a PhpStorm file watcher (point PhpStorm's PHP CS Fixer inspection at .php-cs-fixer.dist.php; it honours the same file).
Pattern F — Big-bang adoption without wrecking git blame. Run the fixer once on a branch, commit only the formatting change, then record the SHA:
echo "<sha>" >> .git-blame-ignore-revs
git config blame.ignoreRevsFile .git-blame-ignore-revs
Also add /.php-cs-fixer.cache to .gitignore — the fixer writes a serialized cache of file hashes by default.
*.php. foo.blade.php satisfies a *.php glob, and the fixer will happily rewrite your template directives as though they were expressions. Always add ->notName('*.blade.php') (and ->notName('*.blade.php')-style rules for any generated stubs) before the first run. This is the single most common way people corrupt a Laravel repo with any PHP CS Fixer setup.pint then php-cs-fixer fix produces an endless edit war and noisy diffs in every PR. Pick one as the writer, and if you keep both, give them separate configs and separate scopes (--path-mode=intersection + distinct directories), never the same file.->forProjects() inserts a file header. The header_comment fixer rewrites the docblock at the top of every file, so your "just formatting" PR touches the whole repo. Either accept it as a one-off commit, or neutralise it: pass your own header via the options array / ->withHeader(...), or override with ->withRules(['header_comment' => false]). ->forLibrary() is the lighter alternative when you don't want a banner.CodeIgniter4Risky ruleset changes runtime behaviour (declare_strict_types is the one that bites — adding it to legacy code turns silent coercions into TypeErrors). Run risky adoption as its own PR, with the full test suite green, never bundled with the cosmetic commit. If you copy risky fixers into pint.json, you must also set "risky": true there.ordered_class_elements, ordered_imports, method_argument_space (on_multiline: ensure_fully_multiline), and class_attributes_separation will rewrite method bodies' argument lists. Land them on a quiet branch, right after a release, and tell the team to rebase or re-run the fixer rather than hand-resolving..php-cs-fixer.cache is keyed on file mtime + config hash; after a PHP upgrade, a dependency bump, or a config edit made outside the file, it can report "0 files fixed" on code that is still non-compliant. Whenever behaviour surprises you, re-run with --using-cache=no before investigating anything else.--dry-run exits 8 when files need fixing (distinct from 1 for a general error, 16/32 for fixer/config failures). If you wrap the command in your own shell script, test for 8 explicitly instead of != 0, otherwise a genuine crash reads as "just needs formatting".php-cs-fixer major line. If your project already constrains php-cs-fixer or an older Pint, Composer will either refuse or silently downgrade you to a fixer version missing rules the ruleset references. Install it first in a scratch branch and read composer why php-cs-fixer before rolling out.vendor/bin/php-cs-fixer describe <rule_name> prints the rule's options and examples, and vendor/bin/php-cs-fixer fix --rules=<rule_name> --dry-run --diff path/to/File.php applies just that rule. This is far faster than bisecting the whole preset when one file keeps flip-flopping.withRules() / the options array, not forking. Add or disable fixers in your own config (Factory::create(new CodeIgniter4(), $options)->withRules([...])) so upgrades stay painless. If you fork the ruleset class you inherit every upstream change as a merge conflict.--dev and build deploy artefacts with composer install --no-dev; the fixer and its rulesets should never ship in vendor/ on a web server._ide_helper*.php, bootstrap/cache, storage/framework/views, and any published package stubs; the fixer will reformat them and your next php artisan ide-helper:generate will undo it, producing churn on every build.How can I help you explore Laravel packages today?