Duster: Run Pint, PHPStan, and Rector with One Command

Laravel Duster runs Pint, PHPStan, Rector, and TLint behind a single command. Add tools via duster.json and run a fast subset with the --using flag in CI.

Steven Richardson
Steven Richardson
· 7 min read

Open the scripts block of a mature Laravel app and you'll usually find four code-quality commands: Pint for formatting, PHPStan for static analysis, Rector for refactors, and maybe TLint for Laravel conventions. Four tools, four configs, four CI steps — and four slightly different ways to invoke them. Laravel Duster, from Tighten, collapses that into one duster lint and one duster fix, with a single config file to add or reorder tools.

I've run it on a few projects now. It's a genuine simplification, but only past a certain amount of tooling. Here's what it bundles, how to plug PHPStan and Rector in, and where the line sits.

What Laravel Duster bundles#

Out of the box, Duster is a wrapper around four tools, each with a Tighten preset applied:

  • Pint — Laravel's formatter, using the Laravel preset with a few Tighten tweaks.
  • PHP_CodeSniffer — sniffs issues that can't be auto-fixed, using a Tighten ruleset that's mostly PSR-1.
  • PHP CS Fixer — adds rules Pint doesn't cover, like Tighten's custom class-element ordering.
  • TLint — Tighten's own linter for Laravel conventions the others miss.

Install it as a dev dependency and you get two commands: one that reports, one that fixes.

composer require tightenco/duster --dev

# Report style violations without touching anything
./vendor/bin/duster lint

# Apply every fixable change in place
./vendor/bin/duster fix

lint is your CI gate — it exits non-zero when something's off and changes nothing. fix is what you run locally before committing. That report-versus-apply split runs through everything else Duster does, including the tools you add yourself. As of Duster 3.4, the package requires PHP 8.2 or newer; older projects are stuck on the 2.x line.

Adding PHPStan and Rector with duster.json#

PHPStan and Rector aren't bundled — and this trips people up, because the brief-level pitch of "one command for everything" implies they are. You add them yourself through a duster.json file in your project root, under the scripts key. Each entry is a command name mapped to an array of process arguments, and the whole thing is split into lint and fix.

{
    "scripts": {
        "lint": {
            "phpstan": ["./vendor/bin/phpstan", "analyse", "--no-progress"],
            "rector": ["./vendor/bin/rector", "process", "--dry-run"]
        },
        "fix": {
            "rector": ["./vendor/bin/rector", "process"]
        }
    },
    "processTimeout": 300
}

The split does real work here. PHPStan only ever analyses, so it belongs under lint alone. Rector runs --dry-run under lint — reporting what it would change — and actually applies those changes under fix. That's the same dry-run-first discipline I lean on when automating Laravel upgrade refactors with Rector, and it maps cleanly onto Duster's two commands. Point PHPStan at whatever config you already have; if you're mid-migration, that's the same setup you'd use when burning down a PHPStan baseline on a legacy app.

Note the processTimeout bump to 300. Custom scripts default to a 60-second timeout, and PHPStan or Rector on a codebase of any size will blow straight past a minute. This is the single most common Duster complaint, and it's a one-line fix.

Running a subset with --using#

Running all six tools on every git commit gets slow fast. The --using flag takes a comma-separated list of tool names and runs only those, in the order you give:

# Fast pre-commit pass: formatting and Laravel lints only
./vendor/bin/duster lint --using=pint,tlint

# Static-analysis pass on its own
./vendor/bin/duster lint --using=phpstan,pint

The names match Duster's built-in tools (pint, tlint) plus any custom script keys you defined (phpstan, rector). One catch worth internalising: --using is an override, not a filter. When you pass it, only the named tools run — your configured PHPStan and Rector scripts are skipped unless you list them. That's exactly what you want for a quick local pass, but don't rely on it in CI, where you want the full set.

For committing only what you've touched, reach for --dirty instead, which restricts Duster to files with uncommitted Git changes:

./vendor/bin/duster lint --dirty

That pairs nicely with a pre-commit hook — a fast formatting pass on changed files, with the heavier static analysis left to the pipeline. Speaking of which, Duster can scaffold a CI workflow for you:

./vendor/bin/duster github-actions

Or wire it in by hand alongside your other checks — the same runner where you might already shard a Pest suite across parallel CI jobs:

# .github/workflows/duster.yml
name: Duster
on: [push, pull_request]

jobs:
  duster:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
      - run: composer install --no-interaction --prefer-dist
      - run: ./vendor/bin/duster lint

When Laravel Duster is worth it (and when it isn't)#

Duster earns its place when you're already running three or more of these tools and want one entry point across your editor, your pre-commit hook, and CI. One command, one duster.json, one mental model. If you like Tighten's opinionated presets, you also get a sensible baseline for free.

It's the wrong call in two cases. First, if your project only runs Pint: you'd be adding a wrapper, three extra tools, and Tighten's opinions to replace a single command that already does its one job well. Second — and this is the honest trade-off — Duster's value is the default presets. The moment you need your own rules, you're back to maintaining a pint.json, a .phpcs.xml.dist, and a PHP CS Fixer config, just routed through Duster. You can absolutely override each tool, but every override you write chips away at the "one config" benefit until what's left is "one command." That's still worth something; just know that's the trade you're making.

For context on the individual tools, it's the same calculation you'd run before adding Peck to catch spelling mistakes in identifiers — each tool has to pay for the config it costs you.

Gotchas and Edge Cases#

Don't run duster fix in CI and commit the result. Tighten ships a duster-action, but workflows that commit fixes back to your repo will halt any currently running workflow and won't trigger a fresh run. If you go that route, chain your other jobs with a workflow_run trigger. The simpler path: run duster lint in CI and let it fail the build, then fix locally.

PHP_CodeSniffer 4.0 is now the dependency. Duster 3.4 pulls in squizlabs/php_codesniffer: ^4.0, a major bump from the 3.x line. If you maintain a custom .phpcs.xml.dist, confirm your ruleset still resolves — a handful of sniffs moved or were renamed between PHPCS 3 and 4.

The 60-second timeout, again. It's worth repeating because it doesn't fail loudly — a timed-out Rector run looks a lot like a passing one until you notice nothing changed. Set processTimeout the moment you add a heavy script.

Wrapping Up#

Reach for Duster when you're maintaining a pile of code-quality commands and want to hide them behind duster lint and duster fix. Add PHPStan and Rector through duster.json, keep a fast --using subset for pre-commit, and run the full set in CI. If you're a Pint-only shop, skip it.

Once Duster is your single entry point, the next win is deepening what each tool catches — pushing PHPStan toward level 10 on a Laravel codebase, or making your test suite type-safe with PHPStan-checked Pest tests.

FAQ#

What is Duster in Laravel?

Duster is Tighten's opinionated linter and fixer for Laravel. It wraps four tools — Pint, PHP_CodeSniffer, PHP CS Fixer, and TLint — behind a single duster lint command that reports issues and a duster fix command that auto-fixes them, applying Tighten's presets by default. You install it as a dev dependency with Composer and run it locally or in CI.

How is Duster different from Laravel Pint?

Pint is a single, opinionated formatter that handles code style. Duster is a wrapper that runs Pint alongside PHP_CodeSniffer, PHP CS Fixer, and TLint, and lets you bolt on tools like PHPStan and Rector. If formatting is all you need, Pint on its own is simpler and lighter. Duster earns its keep once you want several tools behind one command and one config.

Can Duster run PHPStan and Rector?

Yes, though neither is bundled. You register them under the scripts key in duster.json, split into lint and fix arrays, and Duster runs them alongside its built-in tools. Because that split separates reporting from applying, you can run Rector in dry-run mode for lint and let it apply changes for fix, while PHPStan sits under lint only since it never modifies code.

How do I run only some tools with Duster?

Pass the --using flag a comma-separated list of tool names, such as duster lint --using=pint,tlint. Only the named tools run, which makes it perfect for a quick pre-commit pass that skips slower static analysis. The names correspond to Duster's built-in tools plus any custom script keys you defined, and the flag overrides the default set rather than filtering it.

Should I use Duster or run my linters separately?

If you already juggle three or more tools across pre-commit and CI, Duster's single command and shared config are worth the switch. If you run only Pint, or you've heavily customised each tool's standalone config and already wired them into CI, running them separately is perfectly fine — at that point Duster mostly adds a layer of indirection without removing much work.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.