"Clean Code" in Laravel: 6 Practical Tips with Examples

"Clean Code" in Laravel: 6 Practical Tips with Examples
Admin
Monday, August 19, 2024 5 mins to read
Share
"Clean Code" in Laravel: 6 Practical Tips with Examples

Clean code is something all devs aim for, right? But what does it ACTUALLY mean? In this tutorial, I will list 6 practical tips for Laravel to write clean code.

The general goal here is to commit code that is easy for future developers (including ourselves) to understand and maintain.

So, what are the main principles, applied to Laravel projects?


Tip 1. Naming Things: Meaning and Conventions

There's an old saying by Phil Karlton:

"There are only two hard things in Computer Science: cache invalidation and naming things."

So yeah, often, we find it hard to understand someone's code because of poorly worded variables, functions, classes, and methods.

It costs time and effort to understand what the code actually does.

Let's take a look at an example code:

public function create()
{
$aid = request('aid', setting('default.account'));
$std = request('std', Date::now()->firstOfMonth()->toDateString());
$etd = request('etd', Date::now()->endOfMonth()->toDateString());
 
$acc = Account::find($acc_id);
 
$c = $acc->currency;
 
$tl = $this->getTransactions($acc, $std, $etd);
 
$ob = $this->getOpeningBalance($acc, $std);
 
return view('banking.reconciliations.create', compact('acc', 'c', 'ob', 'tl'));
}

What do you think? Can you quickly understand what the variables are? Or think about the View file:

{{ $c->symbol }} {{ $ob }}

What is $c? What is $ob? It's not clear, right?

And now, let's look at appropriately named variables:

public function create()
{
$accountId = request('account_id', setting('default.account'));
$startedAt = request('started_at', Date::now()->firstOfMonth()->toDateString());
$endedAt = request('ended_at', Date::now()->endOfMonth()->toDateString());
 
$account = Account::find($accountId);
 
$currency = $account->currency;
 
$transactions = $this->getTransactions($account, $startedAt, $endedAt);
 
$openingBalance = $this->getOpeningBalance($account, $startedAt);
 
return view('banking.reconciliations.create', compact('account', 'currency', 'openingBalance', 'transactions'));
}

And the View file:

{{ $currency->symbol }} {{ $openingBalance }}

Now, it's so much easier to understand what the code does and the purpose of each variable.

This also applies to functions and classes. Use meaningful names that describe what the function does or what the class is responsible for.

Don't be afraid to use long names, as long as they are descriptive. It's better to have a long descriptive name than a short, cryptic one. Characters are free, but brain capacity is not. For example:

public function getTransactionsForUserWithCurrency(Account $account, string $started_at, string $ended_at)
{
// code here
}

Rather than:

public function transactions($a, $std, $etd)
{
// code here
}

When named clearly, you/anyone can easily understand what the function does without looking at its implementation or documentation.

Of course, you should also follow conventions for naming. They help people understand your code faster and avoid confusion while working in a team. Some of the resources you can use:

There are many different styles per language/framework available for you to choose from. The most important thing is to be consistent with your naming conventions.


Tip 2. Single Responsibility principle: Write Code that Does ONE Thing Only

If a class/method is responsible for many operations, future developers may get lost and find it hard to maintain or fix bugs.

Let's take a look at an example:

class UserController {
 
public function store(Request $request)
{
$this->validate($request, [
'name' => ['required'],
'email' => ['required','email'],
'password' => ['required'],
'country' => ['required'],
'city' => ['required'],
'address' => ['required'],
]);
 
$formatName = explode(' ', $request->input('name'));
$fullAddress = $request->input('address') . ', ' . $request->input('city') . ', ' . $request->input('country');
 
$user = User::create([
'first_name' => $formatName[0],
'last_name' => $formatName[1],
'email' => $request->input('email'),
'password' => bcrypt($request->input('password')),
'full_address' => $fullAddress,
'address' => $request->input('address'),
'country' => $request->input('country'),
'city' => $request->input('city'),
]);
 
$user->notify(new OnboardingEmail());
 
$user->balanceAccount()->create([
'balance' => 0,
]);
 
return redirect()->route('home');
}
}

This code does too many things at once:

  • Validation
  • Formatting the name
  • Creating a user
  • Sending an email
  • Creating a balance account

Let's try to refactor it:

class UserController {
 
public function store(Request $request)
{
$this->validateRequest($request);
 
$user = $this->createUser($request);
 
$this->sendOnboardingEmail($user);
 
$this->createBalanceAccount($user);
 
return redirect()->route('home');
}
 
private function validateRequest(Request $request): void
{
$this->validate($request, [
'name' => ['required'],
'email' => ['required','email'],
'password' => ['required'],
'country' => ['required'],
'city' => ['required'],
'address' => ['required'],
]);
}
 
private function createUser(Request $request): User
{
$formatName = $this->formatName($request->input('name'));
 
return User::create([
'first_name' => $formatName[0],
'last_name' => $formatName[1],
'email' => $request->input('email'),
'password' => bcrypt($request->input('password')),
'full_address' => $this->createFullAddress($request),
'address' => $request->input('address'),
'country' => $request->input('country'),
'city' => $request->input('city'),
]);
}
 
private function formatName(string $name): array
{
return explode(' ', $request->input('name'));
}
 
private function createFullAddress(Request $request): string
{
return $request->input('address') . ', ' . $request->input('city') . ', ' . $request->input('country');
}
 
private function sendOnboardingEmail(User $user): void
{
$user->notify(new OnboardingEmail());
}
 
private function createBalanceAccount(User $user): void
{
$user->balanceAccount()->create([
'balance' => 0,
]);
}
}

Now, the code is split into smaller methods, each responsible for a single action.

This way, it's easier to understand what each method does. It's also easier to test and debug functions separately.

Of course, this is a simple example, and creating private methods in Controllers is not that common. In real-world applications, you will have more complex actions and create separate Service/Action classes. Still, the principle is the same: split your code into smaller methods, each responsible for a single action.


Tip 3. DRY Principle: Refactor Repeating Code

The DRY (Don't Repeat Yourself) principle encourages developers to reduce repetition in code.

If the same code is repeated twice, you will have two places to edit in case of future changes/fixes. There is a higher risk of forgetting to fix one of those places.

Here's an example in a Laravel application, showing a bad code snippet that violates the DRY principle and a good snippet that refactors the code to adhere to DRY.

Bad Code Snippet (Before DRY Refactoring)

Let's consider a scenario where you need to update a user's profile and an admin's profile in two different controllers, but the logic is almost identical.

app/Http/Controllers/UserController.php

public function updateUserProfile(Request $request, $id)
{
$user = User::find($id);
 
$user->name = $request->input('name');
$user->email = $request->input('email');
$user->address = $request->input('address');
$user->phone = $request->input('phone');
 
$user->save();
 
return redirect()->back()->with('success', 'User profile updated successfully!');
}

app/Http/Controllers/AdminController.php

public function updateAdminProfile(Request $request, $id)
{
$admin = Admin::find($id);
 
$admin->name = $request->input('name');
$admin->email = $request->input('email');
$admin->address = $request->input('address');
$admin->phone = $request->input('phone');
 
$admin->save();
 
return redirect()->back()->with('success', 'Admin profile updated successfully!');
}

The problem: if you need to change how profiles are updated (e.g., adding a new field or modifying validation), you'll have to update both methods separately, increasing the risk of errors.

To apply the DRY principle, you can refactor the code by moving the repeated logic into a reusable method or Trait. In this case - we pick a Trait:

app/Traits/UpdatesProfiles.php

namespace App\Traits;
 
use Illuminate\Http\Request;
 
trait UpdatesProfiles
{
public function updateProfile(Request $request, $model)
{
$model->name = $request->input('name');
$model->email = $request->input('email');
$model->address = $request->input('address');
$model->phone = $request->input('phone');
 
$model->save();
 
return redirect()->back()->with('success', 'Profile updated successfully!');
}
}

Our Controllers can now use this Trait:

app/Http/Controllers/UserController.php

namespace App\Http\Controllers;
 
use App\Models\User;
use Illuminate\Http\Request;
use App\Traits\UpdatesProfiles;
 
class UserController extends Controller
{
use UpdatesProfiles;
 
public function updateUserProfile(Request $request, $id)
{
$user = User::find($id);
return $this->updateProfile($request, $user);
}
}

app/Http/Controllers/AdminController.php

namespace App\Http\Controllers;
 
use App\Models\Admin;
use Illuminate\Http\Request;
use App\Traits\UpdatesProfiles;
 
class AdminController extends Controller
{
use UpdatesProfiles;
 
public function updateAdminProfile(Request $request, $id)
{
$admin = Admin::find($id);
return $this->updateProfile($request, $admin);
}
}

Now, any change to the profile update logic needs to be done in one place.

Also, clarity The Controllers are cleaner and more focused on their specific responsibilities, delegating the common functionality to a Trait.


Tip 4. Code Styling: Formatting/Indentation

This tip comes in handy when you work in a team. How often have you dealt with code with inconsistent formatting and indentation? It's a nightmare! Not to mention that git diffs are full of unnecessary changes.

So, how can you avoid this? By following a consistent style guide, of course!

Laravel has its own style guide, but you can also use other popular ones like PSR-2 and PSR-12 or even create your own.

But how do we enforce this? Well, that's the hard part... There's a human factor: first, everyone on the team has to agree to use it.

These days, you can force the usage of Laravel Pint (it comes in Laravel by default now!) to automatically format your code. Here's how you can use it:

./vendor/bin/pint

This will give an output like this:

Once that is done, all the files will have consistent styling. There will be no more arguing about positions, brackets, or spaces. Everyone will be happy!

Oh, and of course, no more git diffs with unnecessary changes!

If you want to read more about code styling, here's another tutorial: Code Styling in Laravel: 11 Common Mistakes


Tip 5. Early Returns

A short one.

A quick win for your code can be so-called early returns. Let's look at a code before refactoring:

if ($this->panel) {
if ($this->deployment) {
$this->deployment->addNewMessage('Generation Processing...' . PHP_EOL);
 
$this->panel->load([
'cruds',
]);
 
$service = new PanelService($this->panel);
 
foreach ($this->panel->panelFiles()->where('path', 'like', '%database/migrations%')->get() as $file) {
$service->deleteFile($file);
}
 
// ...
} else {
return;
}
} else {
return;
}

And now, compare it to the refactored code:

if (! $this->panel || ! $this->deployment) {
return;
}
 
$this->deployment->addNewMessage('Generation Processing...'.PHP_EOL);
 
$this->panel->load([
'cruds',
]);
 
$service = new PanelService($this->panel);
 
foreach ($this->panel->panelFiles()->where('path', 'like', '%database/migrations%')->get() as $file) {
$service->deleteFile($file);
}
 
// ...

The code does the same thing but with early returns. Avoiding the if-statement nesting makes the code cleaner and easier to read.


Tip 6. Avoid Magic Numbers/Strings

Another quick one.

We often leave magic numbers or strings in our code, just like these:

public function getTransactions()
{
return Transaction::where('account_id', 1)->get();
}
 
// Or
 
public function getTransactions()
{
return Transaction::where('status', 'approved')->get();
}

And this is fine in the short term, but what if someone makes a typo?

Or, if you need to change the value, you must search the whole codebase and replace it.

So what can you do? You can use constants or Enums:

class Transaction {
const ACCOUNT_ID = 1;
const STATUS_APPROVED = 'approved';
 
public function getTransactions()
{
return Transaction::where('account_id', Transaction::ACCOUNT_ID)->get();
}
 
// Or
 
public function getTransactions()
{
return Transaction::where('status', Transaction::STATUS_APPROVED)->get();
}
}

This way, you can easily change the value in one place and avoid typos.

Of course, you can also create separate classes for Enums, but the main idea is to avoid magic numbers and strings in your code.


Final Tip: Use Laravel

This might sound weird in this article on this website, but you have to use Laravel. No, I mean it: ACTUALLY use Laravel, don't fight the Framework.

This was discussed in a video by Eric Barnes and Matt Stauffer. Eric asked Matt what the biggest mistake he sees Laravel developers make, and Matt's answer was:

Thinking that you know better than the Framework.

And this is actually true. If you start fighting the Framework and reinventing the bicycle with custom code, you will lose many of its integrations and features.

As a result, you may (later) have to write a lot of repeating code that can be done with a single line in Laravel.

So, our final tip is to learn more Laravel features and use them. Your code will be easier to maintain and upgrade over time.

Any more tips for clean code you would add in the comments?