Every Laravel project I start gets the same four dev dependencies before the first feature branch. Not because I enjoy configuring tools, but because the cost of not having them compounds silently until it becomes the kind of mess that takes a sprint to untangle.
The stack is simple: Pint for formatting, Rector for automated refactors, Larastan for static analysis, and Pest for testing. Each one solves a different problem, and together they catch the majority of issues that code review should never have to discuss.
Pint: stop debating code style
Laravel Pint is an opinionated PHP code style fixer built on top of PHP-CS-Fixer. It ships with Laravel by default now, so there is nothing to install. But most teams never configure it beyond the default, and that is a missed opportunity.
The point of Pint is not that one brace style is better than another. The point is that the decision is made once and never revisited.
./vendor/bin/pint
That is the entire workflow. Run it, commit the result. No arguments about indentation, trailing commas, import ordering, or blank lines between methods. Pint decides. The team moves on.
I use the laravel preset as the base and override sparingly:
{
"preset": "laravel",
"rules": {
"concat_space": {
"spacing": "one"
},
"ordered_imports": {
"sort_algorithm": "alpha"
}
}
}
The config lives in pint.json at the project root. That is the only file anyone needs to read to understand what style rules this project follows.
Where Pint fits in the pipeline
I run Pint in CI as a check, not as a fixer:
./vendor/bin/pint --test
This exits with a non-zero code if any file does not match the configured style. The developer fixes locally with ./vendor/bin/pint, commits the result, and pushes again. Pint never auto-commits in CI. The developer sees what changed and owns it.
Most editors can also run Pint on save. That turns formatting into something that never requires conscious effort.
Rector: refactors you should not do by hand
Rector is an automated refactoring tool for PHP. Where Pint fixes style, Rector changes code structure. It upgrades deprecated syntax, modernises PHP constructs, removes dead code, and applies framework-specific transformations.
composer require --dev rector/rector
The initial setup is a rector.php config file at the project root:
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/app',
__DIR__ . '/config',
__DIR__ . '/database',
__DIR__ . '/routes',
__DIR__ . '/tests',
])
->withPhpSets()
->withPreparedSets(
deadCode: true,
codeQuality: true,
typeDeclarations: true,
);
Running it in dry-run mode shows what it would change:
./vendor/bin/rector --dry-run
The output lists every file, every transformation, and the exact diff. When you are satisfied, run without the flag and Rector applies the changes.
What Rector actually catches
The kinds of changes Rector makes are the ones that are correct but tedious:
- Replacing
strpos($haystack, $needle) !== falsewithstr_contains($haystack, $needle) - Adding return type declarations to methods that already return a consistent type
- Removing unused private methods and unreachable code branches
- Upgrading ternary expressions to null coalescing where safe
- Converting
get_class($object)to$object::class - Replacing legacy array syntax with short arrays
Each transformation is individually small. Collectively, they keep a codebase from aging into a style that no longer matches the PHP version it runs on.
Upgrading PHP and Laravel versions
Rector’s most valuable use case is version migrations. When you move from PHP 8.1 to 8.3 or from Laravel 10 to 11, Rector has rule sets that handle the mechanical changes — the ones that are safe, well-defined, and would take hours to do file by file.
->withPhpSets(php83: true)
This is not a replacement for reading the upgrade guide. It is a way to automate the parts of the guide that say “replace X with Y across your codebase.”
Larastan: catch bugs without running the code
Larastan is a PHPStan wrapper tailored for Laravel. It analyses your code statically — without executing it — and finds type errors, undefined methods, incorrect return types, and logic mistakes that PHP itself would only catch at runtime.
composer require --dev larastan/larastan
The config goes in phpstan.neon at the project root:
includes:
- vendor/larastan/larastan/extension.neon
parameters:
paths:
- app/
level: 6
Then run:
./vendor/bin/phpstan analyse
Why level matters
PHPStan has levels from 0 (loose) to 9 (strict). Each level adds more checks. Larastan inherits this system.
For existing projects, I start at level 5 or 6 and fix what surfaces. Trying to jump to level 9 on a mature codebase is a recipe for a 500-error-count first run that nobody wants to fix.
For new projects, I start at level 8 and aim for 9. When you write code with strict analysis from the start, the rules feel natural rather than punitive.
The progression looks like this:
Level 5: Catches undefined variables, wrong argument counts, basic type mismatches
Level 6: Checks return types and missing type hints
Level 7: Validates union types and partially known types
Level 8: Reports null safety issues, enforces strict types
Level 9: Full strictness — no mixed types allowed
What Larastan finds that tests often miss
Tests verify that code works for the cases you thought of. Larastan verifies that code is structurally sound for cases you did not think of.
A method that returns User|null but whose caller never checks for null will pass every test where the user exists. Larastan flags it immediately.
A query scope that accepts string $status but gets called with an integer somewhere in a controller — that will work in PHP because of type juggling. It will fail silently in edge cases. Larastan catches it before it reaches production.
The most useful catches are not exotic type theory. They are the mundane mistakes: calling a method on a possibly-null object, passing the wrong number of arguments to a function, using a variable that was only defined inside one branch of an if-else.
Baselines for existing projects
If you are adding Larastan to a project that already has thousands of lines, you do not need to fix everything on day one. Generate a baseline:
./vendor/bin/phpstan analyse --generate-baseline
This creates a phpstan-baseline.neon file that records all current errors. From now on, PHPStan only reports new errors. Old ones are tracked but ignored until you fix them. The codebase improves on every commit without a massive retrofit sprint.
Pest: tests that read like specifications
Pest is a testing framework built on top of PHPUnit. It uses a functional syntax that cuts the boilerplate PHPUnit requires and makes test files shorter, clearer, and faster to write.
composer require --dev pestphp/pest pestphp/pest-plugin-laravel
./vendor/bin/pest --init
A Pest test reads like a sentence:
it('creates a user with valid data', function () {
$response = $this->postJson('/api/users', [
'name' => 'Satheez',
'email' => 'satheez@example.com',
'password' => 'password123',
]);
$response->assertStatus(201)
->assertJsonPath('data.name', 'Satheez');
$this->assertDatabaseHas('users', ['email' => 'satheez@example.com']);
});
Compare that to the PHPUnit equivalent:
class CreateUserTest extends TestCase
{
public function test_it_creates_a_user_with_valid_data(): void
{
$response = $this->postJson('/api/users', [
'name' => 'Satheez',
'email' => 'satheez@example.com',
'password' => 'password123',
]);
$response->assertStatus(201)
->assertJsonPath('data.name', 'Satheez');
$this->assertDatabaseHas('users', ['email' => 'satheez@example.com']);
}
}
The logic is identical. The noise is not. No class declaration, no method visibility, no void return type on a test, no test_ prefix convention. Pest keeps the test body and removes the ceremony.
Datasets replace copy-pasted tests
Pest datasets let you run the same test with multiple inputs without duplicating the test body:
it('rejects invalid email formats', function (string $email) {
$response = $this->postJson('/api/users', [
'name' => 'Test User',
'email' => $email,
'password' => 'password123',
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['email']);
})->with([
'missing @' => ['invalid-email'],
'missing domain' => ['user@'],
'spaces' => ['user @example.com'],
'double @' => ['user@@example.com'],
]);
Four test cases, one test body. Each dataset entry gets its own name in the output, so failures are specific.
Architecture testing
Pest’s architecture plugin lets you enforce structural rules as tests:
arch('controllers do not use Eloquent directly')
->expect('App\Http\Controllers')
->not->toUse('Illuminate\Database\Eloquent');
arch('models extend the base model')
->expect('App\Models')
->toExtend('Illuminate\Database\Eloquent\Model');
arch('actions are final classes')
->expect('App\Actions')
->toBeFinal();
These tests enforce conventions that code review would otherwise need to catch manually. If someone adds a raw Eloquent query inside a controller, the test suite catches it before the PR is opened.
Running them together
These four tools compose into a single CI check that covers formatting, structural quality, type safety, and behavior:
# Example GitHub Actions step
- name: Code Quality
run: |
./vendor/bin/pint --test
./vendor/bin/rector --dry-run
./vendor/bin/phpstan analyse
./vendor/bin/pest --parallel
The order matters. Pint is fastest and catches the most trivial issues. Rector finds structural upgrades. Larastan finds type errors. Pest runs the full test suite. If any step fails, the pipeline stops early.
Locally, I run the same sequence before pushing:
./vendor/bin/pint && ./vendor/bin/rector --dry-run && ./vendor/bin/phpstan analyse && ./vendor/bin/pest
Each tool should have its own Composer script so you can run them individually or together:
{
"scripts": {
"lint": "./vendor/bin/pint --test",
"format": "./vendor/bin/pint",
"refactor": "./vendor/bin/rector --dry-run",
"refactor:fix": "./vendor/bin/rector",
"analyse": "./vendor/bin/phpstan analyse",
"test": "./vendor/bin/pest --parallel",
"check": [
"@lint",
"@refactor",
"@analyse",
"@test"
]
}
}
Now each tool has a memorable alias:
composer lint— check formatting without changing filescomposer format— fix formatting in placecomposer refactor— preview Rector changescomposer refactor:fix— apply Rector changescomposer analyse— run Larastancomposer test— run Pestcomposer check— run the full quality gate in sequence
Individual scripts are useful during development. You do not always need the full suite. When you are writing a new feature, composer test is enough. When you are preparing a PR, composer check runs everything. The combined script references the individual ones with the @ prefix, so there is no duplication.
The cost of not having them
None of these tools take more than a few minutes to install. The config files are small. The CI step adds seconds, not minutes.
The cost of skipping them is distributed and invisible. It shows up as inconsistent formatting in pull requests. It shows up as deprecated PHP patterns that survive for years. It shows up as null pointer errors in production that a type checker would have caught at commit time. It shows up as a test suite that nobody trusts because it is too verbose to read.
Each tool alone is useful. Together, they set a quality floor that every commit must clear. That floor is not about perfectionism. It is about making the codebase predictable enough that the team can focus on the problems that actually require human judgement.
I install Pint, Rector, Larastan, and Pest before the first feature because they are cheaper to set up on day one than to retrofit on day ninety. The code does not need to be perfect. It needs to be consistent, type-safe, and tested. These four tools make that the default instead of the aspiration.