kubawerlos/php-cs-fixer-custom-fixers
A collection of custom fixers for PHP CS Fixer that adds extra coding-standard rules beyond the built-in set. Install via Composer, register the custom fixers, and enable rules for cleaner, more consistent PHP code.
This package is not a Laravel package — it is an extension pack for PHP CS Fixer. Nothing happens at runtime, so it belongs in require-dev and only pays off if you actually run a fixer binary in your workflow.
composer require --dev friendsofphp/php-cs-fixer kubawerlos/php-cs-fixer-custom-fixers
Create .php-cs-fixer.dist.php in the project root:
<?php
use PhpCsFixer\Config;
use PhpCsFixer\Finder;
use PhpCsFixerCustomFixers\Fixer;
use PhpCsFixerCustomFixers\Fixers;
return (new Config())
->registerCustomFixers(new Fixers()) // <- without this line, every rule below is "unknown"
->setRiskyAllowed(true)
->setRules([
'@PSR12' => true,
// safe, high-value first picks
Fixer\NoDuplicatedImportsFixer::name() => true,
Fixer\NoUselessCommentFixer::name() => true,
Fixer\NoSuperfluousConcatenationFixer::name() => true,
Fixer\NoTrailingCommaInSinglelineFixer::name() => true,
Fixer\NoUselessParenthesisFixer::name() => true,
Fixer\CommentSurroundedBySpacesFixer::name() => true,
Fixer\DeclareAfterOpeningTagFixer::name() => true,
])
->setFinder(
Finder::create()
->in(__DIR__)
->exclude(['bootstrap/cache', 'storage', 'vendor', 'node_modules'])
->name('*.php')
->notName('*.blade.php')
);
Add a script so the team never types the binary name:
"scripts": {
"lint": "php-cs-fixer fix --dry-run --diff",
"lint:fix": "php-cs-fixer fix"
}
Where to look first: run vendor/bin/php-cs-fixer describe Kubawerlos/no_useless_comment (any fixer name) to get the exact diff semantics and options, then skim vendor/kubawerlos/php-cs-fixer-custom-fixers/src/Fixer/ — each fixer is a single readable class with a getDefinition()/getConfigurationDefinition() that documents its options better than the README.
First use case that pays for itself immediately: NoDuplicatedImportsFixer + NoUselessCommentFixer + NoSuperfluousConcatenationFixer on an existing Laravel app. Run composer lint -- --diff once and you will typically get a few hundred mechanically safe, reviewable changes on day one.
Important Laravel caveat up front: Laravel Pint cannot register custom fixers. Pint has no equivalent of registerCustomFixers(), so Kubawerlos/... keys in pint.json will not work. If your team is on Pint, run Pint for the base preset and PHP CS Fixer (with this package) as a second pass — or keep the rule sets aligned and only let PHP CS Fixer handle the extras.
Reference rules via ::name(), never string literals. The returned value (the Kubawerlos/... form you'll see in configs and cache output) is stable, but ::name() gives you IDE navigation, refactor safety, and a compile-time-ish failure if a fixer is renamed.
Start curated, not "all fixers". This package ships ~40 fixers of wildly different aggressiveness, and several are risky. A pattern that works well in a Laravel codebase:
$safe = [
Fixer\CommentSurroundedBySpacesFixer::name(),
Fixer\ConstructorEmptyBracesFixer::name(),
Fixer\EmptyFunctionBodyFixer::name(),
Fixer\MultilineCommentOpeningClosingAloneFixer::name(),
Fixer\NoDuplicatedArrayKeyFixer::name(),
Fixer\NoDuplicatedImportsFixer::name(),
Fixer\NoImportFromGlobalNamespaceFixer::name(),
Fixer\NoLeadingSlashInGlobalNamespaceFixer::name(),
Fixer\NoSuperfluousConcatenationFixer::name(),
Fixer\NoTrailingCommaInSinglelineFixer::name(),
Fixer\NoUselessCommentFixer::name(),
Fixer\NoUselessDirnameCallFixer::name(),
Fixer\NoUselessParenthesisFixer::name(),
Fixer\FunctionParameterSeparationFixer::name(),
];
then opt into risky ones per-directory rather than globally (see below).
Split configuration by path. PHP CS Fixer takes one rule set per config file, but you can run it twice with different configs — this is the cleanest way to apply PhpUnit* fixers only to tests/:
// .php-cs-fixer.tests.php
return (new Config())
->registerCustomFixers(new Fixers())
->setRiskyAllowed(true)
->setRules([
Fixer\PhpUnitAssertArgumentsOrderFixer::name() => true,
Fixer\PhpUnitDedicatedAssertFixer::name() => true,
])
->setFinder(Finder::create()->in(__DIR__.'/tests'));
"lint:fix": [
"php-cs-fixer fix",
"php-cs-fixer fix --config=.php-cs-fixer.tests.php"
]
CI gate pattern. Use the cache locally, kill it in CI so a stale cache never hides a regression, and fail the build on any diff:
vendor/bin/php-cs-fixer fix --dry-run --diff --using-cache=no -v
For pre-commit hooks (CaptainHook, Husky, lint-staged, GrumPHP), restrict work to staged files:
vendor/bin/php-cs-fixer fix --path-mode=intersection --diff --using-cache=yes $(git diff --cached --name-only --diff-filter=ACM -- '*.php')
Gradual rollout on a legacy app. Fix one rule at a time and land it as its own PR:
vendor/bin/php-cs-fixer fix --rules=Kubawerlos/no_superfluous_concatenation --dry-run --diff app/
Because PHP CS Fixer applies each fixer once per file in a single pass, enabling a new fixer can expose a second-order opportunity for another fixer. Practical pattern: after composer lint:fix, run composer lint again and commit any residual diff — or just accept that CI's first run may need a follow-up pass.
Pair with static analysis, not against it. Laravel projects lean on PHPStan/Larastan docblocks (@property, @method, generics on Collection). Keep those paths out of PhpdocNoSuperfluousParamFixer and NoUselessCommentFixer scopes; a two-config split (app/ vs app/Models/) is usually cheaper than arguing about it.
Discovering fixers programmatically. Fixers is an iterator of fixer instances, so you can build a rules array and filter by risk instead of hand-maintaining lists:
$rules = [];
foreach (new Fixers() as $fixer) {
if (!$fixer->isRisky()) {
$rules[$fixer->getName()] = true;
}
}
This is also the pattern to use when you want a "report only" inventory of everything the package would change, before committing to a subset.
Kubawerlos/... keys into pint.json expecting them to work, they silently don't (or error as unknown rules). Pint ships its own fixer registration; custom fixers need a real .php-cs-fixer.dist.php. Also beware wrappers like Duster that delegate to Pint — same limitation.->setRiskyAllowed(true), otherwise the config throws The rules contain risky fixers. On the CLI that's --allow-risky=yes. Note this flips risk on globally for that config file — split configs if you only want risky fixers in tests/.php-cs-fixer/shim (common when a tool bundles it), the PhpCsFixerCustomFixers\* classes aren't autoloaded by the shim's isolated autoloader. Add:
require __DIR__.'/vendor/kubawerlos/php-cs-fixer-custom-fixers/bootstrap.php';
Symptom if you forget: Class "PhpCsFixerCustomFixers\Fixers" not found while PHP CS Fixer itself runs fine..php-cs-fixer.cache is written to the project root — gitignore it. After upgrading this package or PHP CS Fixer, if a fixer appears to do nothing, delete the cache and rerun before debugging anything else. This bites people constantly.DataProviderNameFixer, DataProviderReturnTypeFixer, DataProviderStaticFixer, InternalClassCasingFixer, MultilinePromotedPropertiesFixer, NumericLiteralSeparatorFixer, PhpDocPropertySorterFixer are all marked deprecated in favour of core rules (php_unit_data_provider_*, class_reference_name_casing, multiline_promoted_properties, numeric_literal_separator, …). Prefer the core rule when available — the custom one can disappear in a major. Don't let both versions of the same rule run; you'll get churn or a conflict.CommentedOutFunctionFixer (comments out var_dump, dd, dump, …) and NoCommentedOutCodeFixer (deletes commented-out code) fight each other. Pick one. On Laravel, dd/dump are usually caught by a static-analysis rule or a pre-commit grep instead; NoCommentedOutCodeFixer is the safer default.DeclareAfterOpeningTagFixer vs blank_line_after_opening_tag / declare_strict_types (the core rule inserts on a new line; this one moves it up). Enable declare_strict_types and this one, but drop blank_line_after_opening_tag, and verify with --diff.NoLeadingSlashInGlobalNamespaceFixer vs global_namespace_import / fully_qualified_strict_types. Choose one philosophy; mixing them produces noisy, oscillating diffs.NoImportFromGlobalNamespaceFixer will rewrite use DateTime; to \DateTime — check it doesn't collide with your ordered_imports / no_unused_imports expectations.IssetToArrayKeyExistsFixer — isset($data['x']) is false when the value is null; array_key_exists is true. If you have nullable payload keys, this changes behaviour. It also emits a warning on undefined arrays where isset was silent.NoNullableBooleanTypeFixer — Eloquent/DB booleans are frequently null (nullable tinyint columns). Do not enable on models or anything hydrating from the DB.NoReferenceInFunctionDefinitionFixer — breaks by-ref wrappers around preg_match, array_walk, sort, and any macro doing by-ref work.ForeachUseValueFixer — unsafe if the loop body mutates the array or relies on ordering.NoUselessStrlenFixer — assumes strlen isn't overridden and that the operand is a string; mb_strlen semantics differ from === '' only marginally, but custom string wrappers will break it.PhpUnitAssertArgumentsOrderFixer / PhpUnitDedicatedAssertFixer — assume vanilla PHPUnit assertions. Custom test-case base classes and wrapper assertions can be misread.->with() providers, not @dataProvider docblocks or static provider methods. Expect zero coverage there; don't enable the data-provider fixers thinking they'll tidy your Pest suite.NoUselessCommentFixer can strip comments that tooling depends on — Laravel Idea, IDE Helper _ide_helper.php regeneration, and PHPStan all consume docblocks. Exclude app/Models or the whole *.php set that your static analyser reads, then introduce it deliberately.ClassConstantUsageFixer produces false positives on short, common strings ('bar', 'active', 'pending') appearing in unrelated classes. Keep it on app/ only, and never on migrations/seeders.NoSuperfluousConcatenationFixer has two escape hatches worth knowing: allow_preventing_trailing_spaces and keep_concatenation_for_different_quotes. Enable both if your codebase deliberately splits long translation strings or concatenates single- and double-quoted literals for readability/line length.php-cs-fixer fix path/File.php -vvv --diff --dry-run. -vvv prints which fixers were applied and which were skipped. If it's not in the list at all, the problem is registration (registerCustomFixers), a config typo, or the Finder excluding the file — not the fixer.^3.x PHP CS Fixer with an incompatible custom-fixers release is where "unknown rule" errors come from. Upgrade both together and rerun with --using-cache=no.Fixers is just an iterator, so you can compose your own registration object and mix in house fixers implementing PhpCsFixer\Fixer\FixerInterface — useful when you want one registerCustomFixers() call covering both this package and internal rules for things like env() outside config/.--using-cache=no. Narrow the Finder (->exclude(['storage', 'bootstrap/cache'])) rather than trimming fixers.How can I help you explore Laravel packages today?