symplify/easy-coding-standard
Easy Coding Standard runs PHP_CodeSniffer and PHP-CS-Fixer rules together through one simple PHP config file. Install on any PHP 7.2–8.5 project, get parallel speed out of the box, use prepared sets, and fix code with a single command.
Installation:
composer require --dev symplify/easy-coding-standard
Initialize Config:
vendor/bin/ecs init
Generates ecs.php with a PSR-12 baseline.
First Run:
vendor/bin/ecs check src/
Fix issues automatically:
vendor/bin/ecs check src/ --fix
Pick your engine:
ECS ships both the PHP engine and blink. As of 13.3.7 blink has full rule parity — PsrAutoloadingFixer was the last unported rule and is now available. That makes blink a viable default for day-to-day runs rather than a "mostly works" experiment.
Integrate into CI/CD (e.g., GitHub Actions):
- name: Run ECS
run: vendor/bin/ecs check src/ --no-interaction --diff
Add to composer.json scripts:
"scripts": {
"cs-check": "ecs check src/ --no-interaction --diff",
"cs-fix": "ecs check src/ --fix --no-diffs"
}
Note the --no-diffs on the fix script — see Gotchas.
Project-Wide Enforcement:
// ecs.php
return static function (ECSConfig $ecsConfig): void {
$ecsConfig->paths([
__DIR__.'/app',
__DIR__.'/src',
__DIR__.'/routes',
__DIR__.'/database',
]);
$ecsConfig->sets([
SetList::PSR_12,
SetList::PER_CS,
]);
$ecsConfig->skip([
__DIR__.'/bootstrap/cache',
]);
};
Per-Module Rules (for large apps):
$ecsConfig->paths([__DIR__.'/modules/auth']);
$ecsConfig->rules([
NoUnusedImportsFixer::class,
SingleImportPerStatementFixer::class,
NullableTypeDeclarationFixer::class,
]);
These three are worth calling out: they were among the fixers whose configuration blink used to silently ignore. If you configure them (or ModifierKeywordsFixer), blink now actually honours the config.
Custom Rulesets:
$ecsConfig->ruleWithConfiguration(
ClassNameLengthRule::class,
['max' => 30]
);
PER_CS: the set now reliably keeps a blank line between the class-import group and use function / use const groups. If your codebase currently has no separation, expect one clean-up pass; after that it's stable.PsrAutoloadingFixer derives the expected class name from the file path, so run it with the real project root (not a copied temp dir) or it will compute the wrong FQCN.vendor/bin/ecs check --parallel --cache
vendor/bin/ecs check --fix --no-diffs --skip=tests
--fix with diffs locally to eyeball what changed, --no-diffs in automation.--fix is now chatty:
--fix prints a diff per fixed file plus the applied checkers, then the summary — gated behind --no-diffs. This is great for local review and terrible for a 300-file pre-commit hook or CI log. Always pair --fix with --no-diffs in automation.
Timing format is conditional:
Runs under 5s print milliseconds (9952ms-style), runs of 5s or more print seconds with two decimals (9.95 s). Any log scraper or CI metric regex keyed on ms will break after the first slow build. Match on the numeric part, not the unit.
Config modelling changes behaviour:
Rules whose configuration was previously unmodelled (ModifierKeywordsFixer, SingleImportPerStatementFixer, NullableTypeDeclarationFixer, and others) will now change files they used to leave alone under blink. Pin the version in composer.lock and roll this upgrade deliberately.
Conditional @return docblocks:
RemoveSuperfluousReturnNameFixer no longer strips the condition variable from PHPStan conditional return types (@return $arr ? A : B). If an earlier version ran over your codebase, audit the docblock diff — corruption is silent and only bites when PHPStan stops resolving the type.
Import blank lines in PER_CS:
Two fixers fight over the same whitespace (BlankLineBetweenImportGroupsFixer vs NoBlankLineBetweenImportsFixer). The priority bug is fixed, but if you maintain a custom ruleset that re-enables the lower-priority fixer at an explicit priority you can reintroduce the loop.
Laravel-specific quirks:
$ecsConfig->exclude([__DIR__.'/resources/views']);__DIR__.'/app/Console/Commands' explicitly if it's outside your default paths.PSR_12 rules:
$ecsConfig->skip([__DIR__.'/app/Exceptions/Handler.php']);
PHP version mismatches:
ECS 13+ requires PHP 8.1+. On Laravel 7 / PHP 7.4, pin ~12.6:
composer require symplify/easy-coding-standard:^12.6
vendor/bin/ecs check --verbose — full per-file trace.vendor/bin/ecs check --no-diffs --fix — confirm whether a change happened without the diff noise.git diff --stat after a fresh upgrade fix pass to spot unexpectedly large changes caused by newly honoured configs.$ecsConfig->rule(CustomLaravelRule::class);
path set by the runner, with the Fixer interface unchanged. Custom fixers (or ports of PHP-CS-Fixer rules that need the filename, as PsrAutoloadingFixer did) can read it instead of threading the path through manually.$ecsConfig->paths(array_filter([env('ECS_PATHS', __DIR__.'/src')]));
vendor/bin/ecs check --format=checkstyle > ecs-report.xml
--fix on a narrow path, widen paths only after the config-modelling upgrade settles.composer.lock and run ecs --version in CI — output formatting (diff bodies, elapsed units) is now release-sensitive enough that mixed versions produce noisy diffs between machines.vendor/ and node_modules/; prefer blink for full-project runs and the PHP engine when you need to debug a specific rule.// app/Console/Commands/ECSCommand.php
public function handle() {
$exitCode = (new ECS())->run();
exit($exitCode);
}
How can I help you explore Laravel packages today?