laravel-fans/lint
Laravel Fans Lint is a linting package for Laravel that helps you scan your codebase for style and quality issues. Add it to your workflow to catch problems early and keep your code clean and consistent.
composer require --dev laravel-fans/lint
Laravel's package auto-discovery registers the service provider, so no manual config/app.php entry is needed on 5.5+. The package ships a config file that drives which fixer rules run (PSR-12 by default) and which paths are scanned. Publish it before your first run so you can commit it:
php artisan vendor:publish --provider="LaravelFans\Lint\LintServiceProvider"
# produces config/lint.php
Where to look first, in order:
config/lint.php — the rules/preset array (defaults to @PSR12) and the paths/exclude lists. This is the only file you'll touch day to day.php artisan list | grep lint and php artisan lint --help — confirm the exact command name and flags for your installed version. The wrapper exposes a check mode (report only) and a fix mode; flag names have shifted between releases, so trust --help over any README.vendor/laravel-fans/lint/src/ — the command class and the service provider. Small enough to read in one sitting, and it tells you exactly which underlying fixer version is pinned.First use case: run the checker against app/ only, on a clean working tree:
php artisan lint --dry-run
# review the diff, then
php artisan lint --fix
git diff --stat # should be style-only changes
Commit that as a single "apply PSR-12" commit before you add any CI enforcement. Trying to enforce linting on a codebase that has never been formatted turns every PR into a merge conflict.
Composer scripts as the team interface. Don't make people remember flags; give them two verbs:
"scripts": {
"lint": "php artisan lint --dry-run",
"lint:fix": "php artisan lint --fix"
}
Then composer lint locally and composer lint:fix before pushing.
CI as report-only, locally as fix. The pattern that actually survives contact with a team is: CI runs the dry-run variant and fails the build on any violation; developers run the fix variant locally. Never let CI mutate files — a --fix in CI produces a green build that hid a real failure, and in some setups silently rewrites the checkout mid-pipeline.
- run: composer install --no-interaction --prefer-dist
- run: php artisan lint --dry-run
Verify the command returns a non-zero exit code when violations exist (run it against a deliberately misformatted file once). If it only prints a report and exits 0, wrap it: php artisan lint --dry-run | tee /dev/stderr | grep -q 'files? need' && exit 1, or call the underlying fixer directly.
Incremental adoption via the exclude list. On a legacy app, start with paths limited to app/Http, app/Models, and tests/, plus exclude entries for anything generated or vendored (storage/, bootstrap/cache/, database/migrations/ from an old project, IDE helper stubs). Widen the paths directory by directory over several PRs. This is the single biggest lever on whether the package gets adopted or ripped out.
Pre-commit hook. Pair with CaptainHook or a plain composer lint:fix in .git/hooks/pre-commit so the fix happens before the diff exists. Since it's PSR-12, it also pairs naturally with a .editorconfig so editors don't fight the fixer over indentation.
Config-driven rule overrides. For anything project-specific (import ordering, declare(strict_types=1) enforcement, trailing commas in multiline arrays), edit the rules array rather than patching the package. Treat config/lint.php as code: it's reviewed, committed, and versioned with the app.
Pick one source of truth for style. If the team already uses Laravel Pint, this package is largely redundant — Pint is first-party, actively maintained, and defaults to a Laravel-flavoured preset rather than raw PSR-12. Run one or the other. Running both (or PHP_CodeSniffer with a PSR-12 ruleset alongside) produces contradictory fixes and an endless "who reformatted my file" argument. If you keep this package, remove Pint from composer.json and say so in the README.
Scoping for monorepos / modular Laravel. The package operates on paths relative to base_path(), so a module-based app needs either one config with every module path listed, or a small custom command that iterates modules and invokes the fixer per directory. The latter is a reasonable extension point if you have more than a handful of modules.
Maintenance is the headline risk. Last release was November 2022, nine stars, zero dependents. Before committing to it, check the illuminate/support constraint in vendor/laravel-fans/lint/composer.json against your Laravel version. On Laravel 10/11/12 you may need --with-all-dependencies, or you may find the service provider references APIs that have moved. This is a dev-only dependency, so a failure is annoying rather than fatal — but it will block composer update at the worst moment. Pin it in composer.lock and be prepared to drop it.
The pinned PHP-CS-Fixer version is the real constraint. The wrapper drags in friendsofphp/php-cs-fixer at whatever version was current in 2022. Newer PHP runtimes (8.3+) can surface deprecation noise or outright break older fixer releases. Check with composer show friendsofphp/php-cs-fixer and read the resolved version before blaming the package for a crash. If the fixer is too old for your PHP, the honest answer is to skip the wrapper and write a .php-cs-fixer.dist.php yourself — you lose an artisan command and gain a maintainable toolchain.
Exit codes are the thing to verify, not the output. Wrappers around fixers frequently print a nice table and return 0. Test it explicitly:
printf '<?php\nclass Foo{}\n' > app/Bad.php
php artisan lint --dry-run; echo "exit: $?"
If exit: 0, your CI is decorative.
config:cache interaction. If your config file calls env() at the top level, php artisan config:cache will bake the value in — usually fine, but it means a LINT_PATHS env var set only in CI won't take effect if the cache was built earlier in the pipeline. Build the cache after setting env, or avoid env() in the lint config.
Path resolution surprises. Paths and excludes are resolved from base_path(), not from the current working directory. Running the command from a subdirectory still scans the whole configured set — usually what you want, occasionally surprising when you expected it to be CWD-scoped.
No auto-fix in the working tree before a commit you care about. --fix rewrites files in place. Run it on a clean tree, and never in the middle of a rebase or a partially staged change — git add -p selections get scrambled by a formatter touching lines you didn't stage.
IDE format-on-save won't respect the config. The rules live in the artisan command's config, not in a .php-cs-fixer.dist.php, so PhpStorm/VS Code PHP-CS-Fixer plugins won't pick them up. Either disable format-on-save and rely on composer lint:fix, or duplicate the ruleset into a .php-cs-fixer.dist.php and accept two places to maintain. The former is less work.
Debugging a rule that won't apply. Dump what's actually loaded rather than guessing:
php artisan tinker --execute="dump(config('lint'));"
If the array is empty, the config wasn't published or the config cache is stale (php artisan config:clear). If it's populated but a rule doesn't take effect, the pinned fixer version may not support that rule name — the fixer silently ignores unknown rules in some configurations.
Extension points, for when the wrapper is too thin. The command class is small and the config is an array of fixer rules, so you can (a) override rules wholesale in config/lint.php, (b) add a custom artisan command that instantiates PhpCsFixer\FixerFactory yourself for special cases (generated code, one-off directories), or (c) drop the wrapper and call the fixer binary from a composer script with the same ruleset. Option (c) is the exit ramp if maintenance stalls — it's a half-hour of work and removes the dependency risk entirely.
How can I help you explore Laravel packages today?