I wanted to know how well my Laravel app followed “the Laravel Way” — the collection of conventions outlined in the Laravel docs, commonly adopted by the community, and derived from analytics on over 50,000 Laravel projects.
Instead of paying $19 to run the Laravel Shift Code Fixer, I audited the codebase myself and fixed the things that actually mattered. Here’s what I found, what I changed, and what I deliberately left alone.
Table of contents
Open Table of contents
What is the Laravel Way?
The Laravel Way is a term coined by Jason McCreary (JMac), the creator of Laravel Shift. It’s a set of practices that make Laravel apps easier to maintain and upgrade. They come from three places:
- The official Laravel documentation — how the framework team writes and recommends code
- Community consensus — patterns adopted by respected developers like Taylor Otwell, Freek Van der Herten (Spatie), Caleb Porzio, and Nuno Maduro
- Real-world data — analytics from the 50,000+ Laravel applications that have been upgraded through Laravel Shift
The idea is simple: if you stick to conventions, your app will be easier for other developers to read, and easier for tools (and humans) to upgrade when new Laravel versions ship.
What I audited
I scanned the entire codebase for the same patterns the Laravel Shift Code Fixer checks for:
- Route definitions (string references vs
::classsyntax) - Facades vs helper functions
- Inline validation vs Form Request classes
- Return type hints on model relationships
- Redundant DocBlocks
- Environment checks
- Hardcoded redirect paths
- Empty or stale directories
What I found
| Pattern | Files affected | Priority |
|---|---|---|
| Missing return types on relationship methods | 12 models, 50+ methods | High |
app()->env == instead of app()->environment() | 2 instances | High |
Auth::id() instead of auth()->id() (inconsistent) | 1 instance | Medium |
Log::error() in a model (plus a typo in the message) | 1 instance | Medium |
Hardcoded /dashboard redirect | 1 instance | Low |
Empty app/Search directory (leftover from removing Algolia) | 1 | Low |
Some things were already correct: routes used ::class syntax, middleware was defined on routes not controllers, and Livewire components followed idiomatic patterns.
The fixes
1. Return type hints on relationships (the big one)
This was by far the largest change — 50+ methods across 12 models. Every relationship method (hasMany, belongsTo, hasOne, belongsToMany, morphMany, hasManyThrough) should declare what it returns.
Before:
public function ideas()
{
return $this->hasMany(Idea::class);
}
public function team()
{
return $this->belongsTo(Team::class);
}
After:
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
public function ideas(): HasMany
{
return $this->hasMany(Idea::class);
}
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
The code inside each function is identical. The only addition is : HasMany or : BelongsTo after the parentheses, plus the corresponding use import at the top of the file.
Return type hints don’t change how your app works. They’re labels that tell PHP (and your code editor) exactly what type of thing a function returns. If someone accidentally changed a relationship to return the wrong type, PHP would throw an error immediately instead of causing a confusing bug later. Your IDE also gets smarter — it can autocomplete methods and highlight errors as you type.
The inconsistency in my codebase was obvious. Some relationships already had return types (like channel(): BelongsTo), while others in the same file didn’t. Laravel’s own make:model generator includes return types on all relationship stubs. This was a clear case of aligning with conventions.
2. Environment checks
Before:
if (app()->env == 'staging') {
Mail::alwaysTo('gm@example.co.uk');
}
if (app()->env == 'local') {
Mail::alwaysTo('dean@example.studio');
}
After:
if (app()->environment('staging')) {
Mail::alwaysTo('gm@example.co.uk');
}
if (app()->environment('local')) {
Mail::alwaysTo('dean@example.studio');
}
The old way does a loose string comparison against a property. The new way uses Laravel’s proper environment() method, which is more reliable and can even check multiple environments at once: app()->environment('staging', 'local').
The irony: the same file already used $this->app->environment('production') a few lines later — it was inconsistent with itself.
3. Named routes instead of hardcoded paths
Before:
return redirect()->to('/dashboard');
After:
return redirect()->route('dashboard');
If the URL for the dashboard ever changes, the named route version keeps working. The hardcoded path breaks silently.
4. Consistent use of helpers
Before (in the Idea model’s boot method):
use Illuminate\Support\Facades\Auth;
'created_by' => Auth::id() ?? 0,
After:
'created_by' => auth()->id() ?? 0,
The auth() helper was already used 93 times across the codebase. This was the one place that imported the Auth facade instead. Consistency won.
5. Fixing a typo along the way
While replacing Log::error() with logger()->error() in the Goal model, I spotted a typo in the log message: '1Error fetching metric data'. That stray 1 had been hiding in production logs, making it harder to grep for real errors. Fixed to 'Error fetching metric data'.
6. Deleting dead code
The app/Search directory was completely empty — a leftover from removing Algolia in a previous session. Deleted.
What I deliberately skipped
Facade-to-helper conversion across the board
The Laravel Shift Fixer would convert all Log::error() to logger()->error(), Cache::remember() to cache()->remember(), and so on. I only did this where it fixed an inconsistency (the Auth and Log cases above).
Why? The Laravel community is genuinely split on this:
- JMac and BaseLaravel advocate helpers — they’re shorter and feel more “Laravel”
- Spatie’s guidelines (spatie.be/guidelines/laravel-php) use Facades consistently — they’re explicit, IDE-friendly, and easier to mock in tests
- The official Laravel docs show both patterns
Both approaches are the Laravel Way. Bulk-converting one to the other adds churn without adding value.
Livewire property type hints
The Fixer would add type hints to Livewire component properties like public $idea. But Livewire has its own rules about property types — a property might intentionally be untyped because it accepts both null and a model during the component lifecycle. Blindly adding types could break Livewire’s reactivity. This needs careful, component-by-component review, not a bulk fix.
The question I asked
When I first considered this, I wanted to understand the tradeoffs before spending any money or time:
I want to run the Laravel code fixer on Growth Method. Investigate 3 possible solutions. Pick your suggested solution and explain why, detail how the various options align or deviate from Laravel conventions and best practices.
The three options that emerged were:
- Run the Laravel Shift Fixer ($19) — battle-tested but all-or-nothing, requires third-party repo access
- Do everything manually — free, full control, but slower and easier to miss things
- Hybrid: run Pint for formatting, manually fix only the substantive issues — free, focused on what matters
I went with option 3. Pint handled formatting (360 files, all clean), and the manual fixes targeted the dozen or so patterns that actually mattered for correctness and consistency.
The question that surprised me
After all the changes were done, I asked a question that revealed I didn’t fully understand what had happened:
Explain what return types are? I don’t understand why these changes are required?
The answer was clarifying: return types are labels, not logic. They don’t change how the code runs. They’re like adding labels to filing cabinet drawers — the files inside are the same either way, but the labels make it easier for everyone (including PHP itself) to find things and avoid mistakes.
Only two changes in the entire session actually affected correctness: the app()->environment() fix and the '1Error' typo. Everything else was about consistency and conventions.
What would Taylor Otwell do?
Taylor’s philosophy boils down to three things:
-
Keep it simple. He’s said he’d “much rather take code out of a project than put it in”. A tool that touches dozens of files for cosmetic changes adds churn without adding value.
-
Stick to conventions, pragmatically. “The Laravel apps that age best are the ones that don’t get too clever.” Fix real inconsistencies; don’t bulk-rewrite working code for style points.
-
Use Pint. Taylor built Laravel Pint as the official answer to code formatting. He didn’t build a conventions fixer — he left that as third-party. That tells you where the line is: formatting is a framework concern; convention-policing beyond that is a team decision.
Taylor would run Pint, fix the handful of real issues, add the missing return types for consistency, and ship it.
Final tally
- 16 files changed across 12 models, 1 controller, 1 service provider
- 50+ relationship methods now have return type hints
- 2 environment checks fixed to use the proper Laravel method
- 1 typo caught in a production log message
- 1 empty directory deleted
- 360 files passed Pint with zero formatting issues
The whole thing took one session. No $19 fee, no third-party repo access, and I understand every change that was made.