palmtree/php-cs-fixer-config
Shared PHP CS Fixer configuration for Palmtree projects, providing an opinionated ruleset for consistent, modern PHP code style. Install via Composer and extend the base config to enforce project-wide formatting standards with minimal setup.
This is a shared PHP CS Fixer ruleset published for Palmtree components — not a fixer itself. It is a thin package whose only job is to return a PhpCsFixer\Config (or a rule array) so every Palmtree project lints identically. With 0 stars and 0 dependents, treat it as an internal convention package: read the source before adopting it.
1. Install it as a dev dependency alongside the fixer.
composer require --dev palmtree/php-cs-fixer-config friendsofphp/php-cs-fixer
Check composer.json in the repo first. Shared config packages usually declare friendsofphp/php-cs-fixer as a suggestion or a wide constraint rather than a hard requirement, so you often need to pin the fixer yourself.
2. Look at these files, in this order:
composer.json — the autoload namespace and the PHP-CS-Fixer version constraint. This tells you whether the config targets the v3 API (PhpCsFixer\Config) or the abandoned v2 (PhpCsFixer\Config under a different namespace/behavior).src/ — the config class and its getRules() method. This is the entire product; it is likely 50–200 lines of rule declarations.README.md — if present, it will show the expected factory/config invocation. If absent (likely at 0 stars), read the class constructor directly.3. Wire up .php-cs-fixer.php at the project root.
<?php
$finder = (new PhpCsFixer\Finder())
->in([__DIR__ . '/src', __DIR__ . '/tests'])
->exclude(['vendor', 'storage', 'bootstrap/cache']);
return (new Palmtree\PhpCsFixerConfig\Config()) // verify the real FQCN
->setFinder($finder);
4. First run — always dry, always diffed.
vendor/bin/php-cs-fixer fix --dry-run --diff --verbose
Expect a large diff on the first pass. Review it before applying; a shared config will reformat files you never intended to touch. Then apply, commit the reformat as a standalone commit so it stays out of feature diffs, and only afterwards enable enforcement in CI.
First use case: new Palmtree component or an existing one being brought onto the standard. Run the dry pass to see the blast radius, commit the mechanical reformat, then add the CI gate.
Extend rather than fork. The intended extension point is subclassing the config and merging rules, so upstream rule changes still reach you:
final class ProjectConfig extends Palmtree\PhpCsFixerConfig\Config
{
public function getRules(): array
{
return array_merge(parent::getRules(), [
'declare_strict_types' => true,
'final_class' => true,
]);
}
}
Use array_merge when you want to win on key collisions; array_replace behaves identically for string keys but is clearer about intent. Never append rules with numeric keys — fixers are keyed by rule name and merging is by key.
Split configs by directory when tests differ from src. Two config files, two CI invocations, is cleaner than one config with per-path overrides:
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.src.php --dry-run --diff
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.tests.php --dry-run --diff
CI gate. The fixer exits non-zero (code 8) when --dry-run finds violations, which fails the build for free:
- run: vendor/bin/php-cs-fixer fix --dry-run --diff --using-cache=no
Use --format=github or --format=checkstyle so violations annotate the PR rather than dumping a wall of diff into the log.
Composer scripts make the workflow discoverable and keep the invocation identical for humans and CI:
"scripts": {
"cs": "php-cs-fixer fix",
"cs:check": "php-cs-fixer fix --dry-run --diff"
}
Ratchet existing codebases. Do not try to fix a legacy repo in one commit. Either scope the Finder to newly touched paths first and widen it per sprint, or apply the full fix once and add the CI gate immediately after so the baseline never regresses. The gate is what makes the ruleset valuable; the one-time reformat is just cleanup.
Pre-commit hook. Only stage the files already in the commit, otherwise the hook reformats the whole tree and produces confusing diffs:
vendor/bin/php-cs-fixer fix --path-mode=intersection $(git diff --cached --name-only --diff-filter=ACM | grep '\.php$')
--path-mode=intersection is the key flag — it constrains the finder to the files you passed.
Editor integration. Point PHPStorm's "PHP CS Fixer" inspection or the VS Code junstyle.php-cs-fixer extension at the same .php-cs-fixer.php. Because the config class is autoloaded from vendor/, the editor must resolve the project's Composer autoloader — set the fixer executable to vendor/bin/php-cs-fixer rather than a global install.
Version pinning. With no releases and no dependents, pin to a commit: "palmtree/php-cs-fixer-config": "dev-main#<sha>". A shared config that silently changes rules under you turns unrelated PRs into reformat diffs.
PHP-CS-Fixer major versions break the config contract. v2 and v3 differ in the Config class namespace, setRiskyAllowed behavior, and rule names (@PSR2 was deprecated for @PSR12). Before adopting, confirm the package's constraint matches the fixer version you installed. Mismatched majors fail with class-not-found or unknown-rule errors that look like a broken install.
Risky rules are opt-in and the package may not enable them for you. If the shared config declares rules like declare_strict_types, strict_param, or void_return without calling setRiskyAllowed(true), they are silently ignored — no error, no fix. Grep getRules() for riskies and confirm the flag is set. This is the single most common "the config isn't doing anything" cause.
Rule conflicts produce confusing cascades. Some fixers fight: no_superfluous_phpdoc_tags strips annotations that phpdoc_* fixers want to add, and ordered_imports can oscillate with no_unused_imports under odd sort strategies. If a file never converges across runs, suspect a conflict. vendor/bin/php-cs-fixer describe <rule_name> explains a rule and its incompatible neighbors.
The Finder must exclude vendor/ explicitly. Older examples omit it and the default include set is not always safe in a Laravel app. Exclude vendor, storage, bootstrap/cache, node_modules, and any generated output. Running the fixer over vendor rewrites third-party code and creates unmergeable git state.
The cache file lies. .php-cs-fixer.cache is keyed by file hash; a stale cache after a rebase or a branch switch can report "no changes" on dirty files. Add .php-cs-fixer.cache to .gitignore and use --using-cache=no in CI. Locally, --clear-cache is the fix.
--diff and -vvv are the debugging tools. --dry-run alone tells you a file is non-compliant; --dry-run --diff shows the exact transformation, and -vvv prints which fixers were applied and which errored. When a run fails with exit code 16 (an error, not a violation), -vvv names the offending file and rule — usually a syntax error or a rule that cannot parse the file.
Exit codes carry meaning. 0 = clean, 8 = violations found in dry-run, 16 = fixer error. CI scripts that treat any non-zero as "lint failed" will conflate a real crash with a formatting diff. Check for 8 specifically if you branch on it.
Target PHP version matters. Some rules assume a minimum PHP version, and future-mode behavior (PHP_CS_FIXER_FUTURE_MODE=1) changes what the fixer will do to modern syntax. Set the PHP_CS_FIXER_FUTURE_MODE env only deliberately, and ensure your CI matrix runs the fixer on a PHP version at least as new as your lowest supported target.
Extension points beyond rules. If you need to change the finder for all projects, override setFinder behavior in a subclass or wrap the config in a project-local factory. Avoid editing vendor/ — the config's methods are the only stable surface, and with no releases there is no changelog to warn you when they shift.
Audit before adopting. At 0 stars and 0 dependents there is no community review, no issue history, and no release tags. Read every rule in getRules() once and confirm each is something you would have chosen. A shared config is a supply-chain dependency for your entire codebase's formatting; the cost of reading 150 lines is trivial compared to inheriting a surprise strict_types sweep across every file.
How can I help you explore Laravel packages today?