The Laravel Boost team just shipped a new skill called laravel-best-practices. It checks your code against 100+ Laravel best practices — Eloquent, caching, queues, authentication — and flags anything that doesn’t follow conventions.
I ran it against Growth Method and worked through every finding. Here’s what it caught, what I fixed, and what I learned.
TLDR: it found 25+ issues across 7 categories. I fixed them all in one session.
Table of contents
Open Table of contents
What is Laravel Boost?
Laravel Boost is an MCP server that gives your AI coding agent direct access to your Laravel app — database schema, error logs, browser logs, and version-specific documentation search. It ships with built-in skills that activate automatically based on the packages in your composer.json.
To install or update Laravel Boost:
composer update laravel/boost
php artisan boost:install
You can view the laravel-best-practices skill definition and the full rules files on GitHub.
What the skill covers
The skill checks your code against 20 rules files:
| Rule | What it checks |
|---|---|
| advanced-queries | Subquery selects and dynamic relationships to avoid N+1 |
| architecture | Single-purpose Action classes and constructor dependency injection |
| blade-views | $attributes->merge(), @pushOnce, Blade components over @include |
| caching | Cache::remember(), Cache::flexible() for stale-while-revalidate, Cache::memo() |
| collections | Higher-order messages, cursor() vs lazy() vs lazyById(), toQuery() |
| config | env() only in config files, encrypted env for secrets |
| db-performance | Eager loading to prevent N+1, constraining eager loads to needed columns |
| eloquent | Correct relationship types with return type hints, local scopes |
| error-handling | Exception reporting/rendering on the class vs centralised in bootstrap/app.php |
| events-notifications | Event discovery, event:cache, ShouldDispatchAfterCommit, queued notifications |
| http-client | Explicit timeout() and connectTimeout(), service-specific Http::macro() clients |
ShouldQueue on Mailables, afterCommit(), Markdown mailables | |
| migrations | Artisan-generated migrations, constrained() foreign keys, never modifying deployed migrations |
| queue-jobs | retry_after > timeout, exponential backoff for retries |
| routing | Implicit route model binding, scoped bindings for nested resources |
| scheduling | withoutOverlapping(), onOneServer(), runInBackground(), Schedule Groups |
| security | Mass assignment protection with $fillable, authorising every action |
| style | Laravel naming conventions, shorter readable syntax (helpers over facades) |
| testing | LazilyRefreshDatabase, model assertions, factory states, Exceptions::fake() |
| validation | Form Request classes over inline validation, array notation for rules |
How the audit worked
I asked Claude Code to run a full audit of my app against the laravel-best-practices skill. It broke the work into six parallel checks:
- Security —
$fillable/$guarded, raw SQL,$request->all(), file uploads,env()misuse - DB Performance — N+1 queries, lazy loading,
SELECT *, queries in Blade - Validation & Routing — inline validation, Form Requests, route definitions, controller size
- Eloquent & Caching — relationship types, casts, caching patterns, config
- Queues, Events & Testing — job config, event patterns, test traits, error handling, scheduling
- Architecture & Style — action classes, HTTP client, migrations, Blade patterns, helpers
The results came back as a single prioritised report with seven priorities, ranked by impact.
What it found (and what I fixed)
Priority 1: Security
TLDR: Without $fillable, if someone passes raw request data to create() or update(), they could set columns you didn’t intend. It’s a defence-in-depth measure — even if your current code is safe, $fillable protects against future mistakes.
The audit found four models without $fillable arrays — meaning all columns were mass-assignable.
Added $fillable arrays to four models:
// app/Models/FileUpload.php
protected $fillable = [
'filename', 'url', 'type', 'file_size', 'ideas_id', 'team_id',
];
Priority 2: Database performance
TLDR: If your code loops through 50 campaigns and accesses $campaign->team without eager loading, Laravel silently runs 50 extra database queries. Missing indexes slow down report pages, and loading everything into memory risks crashes as data grows.
The biggest finding was the lack of Model::preventLazyLoading() in development. Without it, N+1 queries happen silently — your app works, just slower than it should. With preventLazyLoading enabled, it throws an exception instead, so you catch it before production.
Added lazy loading prevention to AppServiceProvider:
// app/Providers/AppServiceProvider.php
Model::preventLazyLoading(! $this->app->isProduction());
Added composite database indexes for report queries:
// Migration
$table->index(['team_id', 'date_test_inprogress']);
$table->index(['team_id', 'date_test_complete']);
$table->index(['team_id', 'date_test_analysing']);
Replaced an in-memory array search with database-side queries for campaign navigation:
// Before: loads every ID into memory
$ids = $this->getIdeasQuery()->pluck('id')->toArray();
$currentIndex = array_search($this->idea->id, $ids);
// After: asks the database directly
$nextIdea = (clone $this->getIdeasQuery())
->where('id', '<', $this->idea->id)
->value('id');
And switched a console command from ->get() to ->chunkById(100, ...) so it doesn’t load every record into memory at once.
Priority 3: HTTP client resilience
TLDR: Without timeouts, a slow external API can freeze your background jobs. Ten seconds is plenty for most API calls.
Every outbound HTTP call was missing explicit timeouts. If a third-party API went slow, queue workers would hang indefinitely.
// Before
Http::withToken($token)->post($url, $data);
// After
Http::timeout(10)->connectTimeout(3)->withToken($token)->post($url, $data);
Priority 4: Queue and event hygiene
TLDR: When a background job fails after three retries, you want to know about it. And when an event fires inside a transaction, you want listeners to wait until the data is actually saved.
Four background jobs had no failed() method. When they exhausted retries, they died silently — no log entry, no notification.
// Added to each job
public function failed(\Throwable $exception): void
{
Log::error('GenerateDesign job failed', [
'idea_id' => $this->idea->id,
'error' => $exception->getMessage(),
]);
}
All ten event classes lacked ShouldDispatchAfterCommit. This matters when events fire inside database transactions — without it, a listener might process data that hasn’t actually been committed yet.
// Before
class ExperimentCompleted implements ShouldBroadcast
{
}
// After
class ExperimentCompleted implements ShouldBroadcast, ShouldDispatchAfterCommit
{
}
Priority 5: Eloquent and model hygiene
TLDR: Adding return type hints helps your IDE autocomplete correctly and catches bugs where a scope forgets to return the query builder. Small changes that make the codebase consistent.
Missing return type hints on model scopes and boot() methods. The Idea model had them correct — the rest needed to match.
// Before
protected static function boot()
// After
protected static function boot(): void
// Before
public function scopeIsIdea($query)
// After
public function scopeIsIdea($query): Builder
Priority 6: Architecture and style
TLDR: Laravel provides readable helpers for common operations. Using them instead of raw PHP makes the code more consistent, more testable, and easier to follow.
Replaced manual instantiation with dependency injection, swapped raw PHP string functions for Laravel’s Str helpers, and cleaned up request syntax.
// Before
$domain = substr(strrchr($email, '@'), 1);
// After
$domain = Str::after($email, '@');
// Before
$state = $request->input('state');
// After
$state = $request->state;
Priority 7: Testing
TLDR: LazilyRefreshDatabase only runs migrations when a test actually hits the database, making your test suite faster. Ten test files were overriding it with the slower default.
Ten Jetstream test files overrode the base test class with RefreshDatabase instead of the faster LazilyRefreshDatabase already configured. Removing the override speeds up the test suite.
Final scorecard
| Priority | Summary |
|---|---|
| 1. Security | Mass-assignment protection ($fillable) |
| 2. DB Performance | Lazy loading prevention, indexes, N+1 fixes, chunking |
| 3. HTTP Timeouts | Explicit timeouts on all outbound HTTP calls |
| 4. Queue/Event Hygiene | failed() methods on jobs, ShouldDispatchAfterCommit on events |
| 5. Eloquent Hygiene | Return type hints on scopes and boot() methods |
| 6. Architecture/Style | Dependency injection, Str helpers, request syntax |
| 7. Testing | Remove redundant RefreshDatabase from Jetstream tests |
A note on manual verification
The audit is thorough, but not infallible. One finding flagged unescaped rich text output ({!! !!}) as an XSS risk and recommended adding HTMLPurifier. After investigating, I dropped it from the list.
TipTap (which powers flux:editor in Flux) is built on ProseMirror, which uses a schema-based approach. It only produces HTML for node types it knows about — paragraphs, headings, bold, italic, lists, links, and so on. If a user pastes <script>alert('xss')</script> into the editor, ProseMirror strips the <script> tag because it doesn’t match any registered node type. The schema acts as an allowlist, not a blocklist.
Livewire adds a second layer. Its snapshot checksum system prevents users from tampering with the wire:model value client-side. A user can’t bypass the TipTap editor and inject arbitrary HTML into the Livewire property without Livewire throwing a CorruptComponentPayloadException.
So the {!! !!} is safe in this case. The lesson: always verify findings against your specific stack before adding complexity.
Adding it to my workflow
After working through all seven priorities, I wanted this check to happen automatically. I added an instruction to my CLAUDE.md project file:
After every commit, run the
laravel-best-practicesskill against the changed files to check for violations before pushing.
Now every commit gets a quick best-practices review before it reaches the remote. It takes seconds and catches things I’d otherwise miss.
The laravel-best-practices skill is a solid addition from the Laravel Boost team. If you’re building with Laravel, run it against your codebase. You’ll find things worth fixing.