November 13, 2025

Writing Laravel Like a Senior: Refactoring Common Junior Mistakes

Tutorial
Tips
Writing Laravel Like a Senior: Refactoring Common Junior Mistakes

Clean, expressive code doesn’t just look good. it scales, performs, and makes sense months later. Let’s compare typical “junior” Laravel code with “senior” refactors that follow best practices, cleaner abstractions, and maintainability.


1. Controllers Doing Too Much

Junior code:

public function store(Request $request) { $validated = $request->validate([ 'title' => 'required', 'body' => 'required', ]); $post = new Post(); $post->title = $validated['title']; $post->body = $validated['body']; $post->user_id = auth()->id(); $post->save(); Mail::to('admin@example.com')->send(new PostCreated($post)); return redirect()->route('posts.index'); }

Senior refactor:

public function store(StorePostRequest $request, CreatePostAction $action) { $post = $action->execute($request->validated()); return to_route('posts.index'); }

Explanation:

  • Moves validation to a Form Request
  • Moves logic to an Action class
  • Controller stays clean and declarative

2. Query Spaghetti

Junior code:

$posts = Post::where('status', 'published') ->where('views', '>', 100) ->whereHas('user', function ($q) { $q->where('active', true); }) ->orderBy('created_at', 'desc') ->get();

Senior refactor:

$posts = Post::published() ->popular() ->byActiveUsers() ->latest() ->get();

Model Scopes:

public function scopePublished($query) { return $query->where('status', 'published'); } public function scopePopular($query) { return $query->where('views', '>', 100); } public function scopeByActiveUsers($query) { return $query->whereHas('user', fn($q) => $q->where('active', true)); }

Why better: Readable, reusable, and self-documenting, you understand what’s happening without squinting.


3. Business Logic in Models

Junior code:

public function purchase($user) { if ($this->stock < 1) { throw new \Exception('Out of stock'); } $this->stock--; $this->save(); Order::create([ 'user_id' => $user->id, 'product_id' => $this->id, 'total' => $this->price, ]); }

Senior refactor: Move logic to a Service or Action:

class PurchaseProductAction { public function execute(Product $product, User $user) { throw_if($product->stock < 1, OutOfStockException::class); DB::transaction(function () use ($product, $user) { $product->decrement('stock'); Order::create([ 'user_id' => $user->id, 'product_id' => $product->id, 'total' => $product->price, ]); }); } }

Benefits:

  • Easier to test
  • Keeps models lightweight
  • Business logic is explicit and maintainable

4. Magic Strings Everywhere

Junior code:

if ($user->role === 'admin') { // ... }

Senior refactor: Use Enums (Laravel 9+):

if ($user->role === UserRole::Admin) { // ... }
enum UserRole: string { case Admin = 'admin'; case Editor = 'editor'; case Viewer = 'viewer'; }

Why better: Enums prevent typos, give IDE autocompletion, and make roles explicit.


5. Unoptimized Relationships

Junior code:

$orders = Order::all(); foreach ($orders as $order) { echo $order->user->name; }

Senior refactor:

$orders = Order::with('user')->get(); foreach ($orders as $order) { echo $order->user->name; }

Why better: Prevents the N+1 query problem — one of the most common performance killers.


6. Helper Chaos

Junior code:

if (isset($data['price'])) { $price = $data['price']; } else { $price = 0; }

Senior refactor:

$price = data_get($data, 'price', 0);

Cleaner, shorter, Laravel-native.


7. Raw Queries for Everything

Junior code:

$users = DB::select('SELECT * FROM users WHERE active = 1');

Senior refactor:

$users = User::active()->get();

Why better: Leverages Eloquent’s query builder and local scopes for cleaner, testable code.


8. No Exception Handling

Junior code:

$product = Product::find($id); if (!$product) { abort(404); }

Senior refactor:

$product = Product::findOrFail($id);

Why better: Simpler, cleaner, and consistent with Laravel’s exception system.


9. No DTOs (Data Transfer Objects)

Junior code:

$orderData = [ 'user_id' => $user->id, 'amount' => $cart->total(), 'status' => 'pending', ]; Order::create($orderData);

Senior refactor:

$order = CreateOrderAction::execute(CreateOrderData::fromCart($cart, $user));

Why better: Encapsulates logic, reduces repetition, and increases reliability when requirements grow.


10. No Tests, No Confidence

Junior habit: Manual testing every feature before pushing.

Senior habit: Automated testing using PHPUnit or Pest:

it('creates a post successfully', function () { $post = Post::factory()->create(); expect($post)->toBeInstanceOf(Post::class); });

Why better: Tests protect your codebase from regressions — senior devs trust tests more than memory.


Summary

Junior code works — but it’s often rigid, hard to test, and hard to extend. Senior code aims for clarity, reusability, and long-term maintainability.

Think less about “does it work?” and more about “will it scale and still make sense a year later?”

Did you find this article helpful? Share it!