Checks a Laravel app against the Laravel Style Guide and reports every finding with its rule ID, linked to the rule:
app/Models/Invoice.php:24:1: MUST LARAVEL-07.3: declare #[Table('...')] on Invoice, even when it matches the convention [chopper]
app/Http/Controllers/InvoiceController.php:31:9: MUST LARAVEL-11.1: validate in a form request, not inline in the controller [chopper]
It runs four kinds of check:
- Laravel Pint with the guide's
pint.json(pint --test, chapter 5). Fixers that enforce a specific rule (declare_strict_types,final_class,strict_comparison, imports, member order, trailing commas) report under that rule. - Larastan (PHPStan) at level 8 with the guide's
phpstan.neon. Missing types report as LARAVEL-05.8,env()outsideconfig/as LARAVEL-17.1, everything else as LARAVEL-05.7. - Rector in dry-run mode, when the app has a
rector.php. - The guide's own checks for rules the tools do not express: attribute-first models (
#[Table],#[Fillable],#[UseFactory],HasUuids, no$guarded), anonymous migrations with UUID keys and explicitonDelete, form requests, thin controllers, named routes, actions and DTOs, job and command attributes, raw SQL with variables,env()outside config,{!! !!}without a sanitising note, Livewire#[Locked]IDs and per-action authorization, debugging calls, naming and more.checks.jsonlists them all.
The tools run from the app's own vendor/bin, so their versions are the ones composer.lock pins: run composer install first. The app's own pint.json and phpstan.neon are used when it has them; otherwise the kit's copies (the guide's appendix, verbatim) are.
Run it
Needs Python 3.9+, and PHP 8.5 with the app's Composer dependencies for the tool checks. Run it from the app's folder (where composer.json is), or pass --root:
python3 chopper_lint.py # the whole app
python3 chopper_lint.py app/Models routes # some folders
python3 chopper_lint.py --no-rector # skip a tool (--no-pint, --no-phpstan)
python3 chopper_lint.py --strict # fail on SHOULD too
python3 chopper_lint.py --list-checks # what it checks
Folders named vendor, node_modules, storage, public, bootstrap/cache and build are skipped. Blade views and the TypeScript under resources/js are checked too.
Exit status: 1 when a MUST or MUST NOT rule is broken (any finding with --strict), else 0.
In GitHub Actions
- uses: actions/checkout@v4
- uses: carson-hopper/chopper.dev/content/styleguides/laravel/lint@main
# with:
# working-directory: backend # the app's folder, if it is not the repository root
# php-version: "8.5" # empty to use the PHP already on the runner
The action installs PHP with shivammathur/setup-php and runs composer install when vendor/ is missing, then lints. Findings appear as annotations on the pull request. Pin @main to a commit SHA for reproducible runs.
Silencing a finding
On the same line or the line above, with a reason:
$totals = DB::select($report->sql()); // chopper-lint: allow LARAVEL-09.9 (vetted report SQL, no input)
For a whole file: // chopper-lint: allow-file LARAVEL-08.2. In Blade: {{-- chopper-lint: allow LARAVEL-19.3 (sanitised by Purifier) --}}. # comments work too; #[...] is an attribute, not a comment.