Benjamin Crozat The web developer life

Laravel best practices for 2022: the definitive guide

Benjamin Crozat's avatar. Benjamin Crozat5 minutes read

Before you start reading this article, did you know 66 persons subscribed to my newsletter?

Join them and enjoy free content about the art of crafting websites!

Laravel best practices for 2022: the definitive guide

This article is a work in progress. It can still be improved, and you can contact me on Twitter for suggestions.

For most Laravel projects, the best practices can be summarized as two points:

But why should you apply these best practices?

First, let me remind you that Laravel is the most used PHP framework in the world (see my article about PHP usage statistics for 2022).

Therefore, there are more job offers than there are developers.

If you want to be hired, you don’t need to be the best Laravel developer in the world, but you need to show that you won’t waste your employer’s time and money writing useless and inefficient code.

By being up to date on Laravel’s best practices, you will be able to collaborate with other developers more easily because just like you, they already know Laravel.

Now, let’s see how you can improve any Laravel project thanks to actionable tips and tricks.

Table of contents

General best practices

Keep Laravel up to date

Keeping Laravel up to date provides the following benefits:

The only way to keep Laravel up to date without the fear of breaking everything is to start writing tests.

Keep packages up to date

Access to thousands of community packages is what makes our job way easier.

But the more packages you use, the more vulnerabilities you might be subject to.

If your codebase is has a good tests suite, regularly running composer update goes a long way toward a convenient, up to date and secure codebase.

Keep your project tested

Writing automated tests (people often talk about unit testing) is a vast and lesser-known topic among developers.

But did you know it’s also the only way to ensure reliability?

What are the benefits of a good tests suite?

Being able to ensure the reliability of your projects with something other than gut instinct will make you an immensely better developer.

Stick to the default directory structure and defer as much work as possible to Laravel

Do you know why you’re using a framework?

  1. It frames your work with a set of guidelines that you can follow to ensure every member of your team is on the same page;
  2. It provides many complex, tedious, and battle-tested features for free, so you can focus on coding what is specific to your project.

When should you not stick to the defaults? When the size of your project actually requires to do things differently.

Controllers best practices

Use form requests

Form requests exist for a few reasons:

  1. Reusing (often complex) validation across multiple controllers;
  2. Offloading code from bloated controllers;
  3. Checking out the app/Http/Requests folder for everything related to validation is natural for everyone.

Use single action controllers for clarity

Sometimes, it makes things easier to use single action controllers.

You can’t always avoid complexity, and this complexity might be better off in its own file.

Use policies

Policies exist for a few reasons:

  1. Reusing authorization logic across multiple controllers;
  2. Offloading code from bloated controllers;
  3. Checking out the app/Policies folder for everything related to authorizations is natural for everyone.

Eloquent best practices

Prevent N+1 issues with eager loading

Eager loading is a great solution to avoid N+1 problems.

Let’s say you are displaying a list of 30 posts with their author:

The fix is simple: use the with() method, and you’ll go from 31 queries to only 2.

Post::with('author')->get();

To ensure you don’t have N+1 problems, you can trigger exceptions whenever you lazy load any relationship. This restriction should be applied to your local environement only.

Model::preventLazyLoading(
! app()->isProduction()
);

Use Eloquent’s strict mode

Eloquent’s strict mode is a blessing for debugging. It will throw exceptions when:

Add this code in the boot() method of your AppServiceProvider.php:

Model::shouldBeStrict(
! app()->isProduction() // Only outside of production.
);

Use the new way of declaring accessors and mutators

The new way of declarating accessors and mutators was introduced in Laravel 9.

This is how you should declare them now:

use IlluminateDatabaseEloquentCastsAttribute;
 
class Pokemon
{
function name() : Attribute
{
$locale = app()->getLocale();
 
return Attribute::make(
get: fn ($value) => $value[$locale],
set: fn ($value) => [$locale => $value],
);
}
}

You can even cache expensive to compute values:

use IlluminateDatabaseEloquentCastsAttribute;
 
function someAttribute() : Attribute
{
return Attribute::make(
fn () => /* Do something. */
)->shouldCache();
}

The old way looks like this:

class Pokemon
{
function getNameAttribute() : string
{
$locale = app()->getLocale();
 
return $this->attributes['name'][$locale];
}
 
function setNameAttribute($value) : string
{
$locale = app()->getLocale();
 
return $this->attributes['name'][$locale] = $value;
}
}

Migrations best practices

First, please know that I wrote an article about Laravel migrations. You might want to check it out.

Use anonymous migrations (Laravel 8 and above)

Anonymous migrations are a great way to avoid class names conflicts.

Laravel generates anonymous migrations for you as long as you’re using Laravel 9 and above:

php artisan make:migration CreatePostsTable

This is how they look:

<?php
 
use IlluminateSupportFacadesSchema;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
 
return new class extends Migration {
}

But did you know you can also use then with Laravel 8? Just replace the class names, and you’ll be good to go!

Use the down() method correctly

The down() (used by the php artisan migrate:rollback command) is ran when you need to rollback the changes you made to your database.

Some people use it, some don’t.

If you belong to the people who use it, you should make sure your down() method is implemented correctly.

Basically, the down() method must do the opposite of the up() method.

use IlluminateSupportFacadesSchema;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
 
return new class extends Migration {
public function up()
{
Schema::table('posts', function (Blueprint $table) {
// The column was a boolean, but we want to switch to a datetime.
$table->datetime('is_published')->nullable()->change();
});
}
 
public function down()
{
Schema::table('posts', function (Blueprint $table) {
// When rolling back, we have to restore the column to its previous state.
$table->boolean('is_published')->default(false)->change();
});
}
}

Performances best practices

Performances come with choosing the right hosting for your projects (such as Cloudways, which is a great alternative to Laravel Forge). I actually wrote about the best hosting providers for Laravel projects.

But optimizing your code is a priority. Slow code is often the #1 cause of slow web applications.

Use dispatchAfterResponse() for long-running tasks

Let’s use the most straightforward example possible: you have a contact form. Sending an email may take between one or two seconds, depending on your method.

What if you could delay this until the user receives your server’s response?

That’s precisely what dispatchAfterResponse() does:

SendContactEmail::dispatchAfterResponse($input);

Or, if you prefer to dispatch jobs using anonymous functions:

dispatch(function () {
// Do something.
})->afterResponse();

Use queues for even longer running tasks

Imagine you have to process images uploaded by your users. If you process every image as soon as they’re submitted, this will happen:

This isn’t good UX, and we can change that.

Laravel has a queue system that will run all those tasks sequentially or with a limited amount of parallelism.

Testing best practices

Lazily refresh your database

When you can get away with fake data in your local environment, the best thing to do is to test against a fresh database every time you run a test.

You can use the IlluminateFoundationTestingLazilyRefreshDatabase trait in your tests/TestCase.php.

There’s also a RefreshDatabase trait, but the lazy one is more efficient, as migrations for unused tables won’t be ran during testing.

Make use of factories

Factories make testing way more manageable. You can create one using the php artisan make:factory command.

Using them, we can create all the resources we need when writing tests.

public function test_it_shows_a_given_post()
{
$post = Post::factory()->create();
 
$this
->get(route('posts.show', $post))
->assertOk();
}

Test against the production stack whenever it’s possible

When running your web application in production, you probably use something other than SQLite like MySQL.

So why are you running your tests using SQLite?

Use MySQL, or you won’t be able to catch bugs that occur only with this database.

Are you running Redis in production as well? Same thing, don’t use the array cache driver.

It’s more important to have a reliable tests suite than a quick one.

Use database transactions

In one of my projects, I need to create a database filled with real data provided by CSV files on GitHub.

It takes time and I can’t refresh my database before every test.

So when my tests alter the data, I want to rollback the changes. You can do so by using the IlluminateFoundationTestingDatabaseTransactions trait in your tests/TestCase.php.

Don’t waste API calls, use mocks

Say you have a service that makes HTTP requests to some web API.

Unless it’s Stripe, which has a test environment, you might want to mock those calls or you will often be hit with rate limits.

Laravel has a great HTTP client with fantastic testing helpers which makes mocking a breeze.

Prevent stray HTTP requests

In case you’re, for instance, mocking an API call, you can prevent any stray request being executed by using the method below:

Http::fakeSequence()
->push(['foo' => 'bar']);
 
Http::preventStrayRequests();

Slow tests are often caused by those stray HTTP requests.

Versioning best practices

Don’t track your .env file

This advice may be obvious for certain people, but I think it’s worth mentionning anyway.

Your .env file contains sensitive informations.

Please, don’t track it!

Make sure it’s included in your .gitignore.

And if at some point you tracked it and untracked it, make sure nobody having access to the Git history can use the credentials that were in it. Change them.

Don’t track your compiled CSS and JavaScript

Compiled CSS and JavaScript are generated based on your original files in your resources folder.

When deploying into production, you either compile them on the server or you create an artifact before.

There’s no need for tracking here.

I share everything I learn about the art of crafting websites, for free!

Other posts to read

A soft delete in Laravel allows you to prevent mistakes by not removing sensitive data from your database right away.

Take your code to the next level, thanks to exceptions. Handle errors in a more graceful way within try and catch blocks.

AI is a trending topic in the programming space. It enables developers to do incredible things, and many startups build products around it.

Let's review some quick wins that Laravel Collections provide to instantly make your codebase better.

Learn why and how to fix "Methods with the same name as their class will not be constructors in a future version of PHP" warnings.

Knowing which Laravel version you are running is important before you start writing code on a new project. There are multiple ways to do so.

switch, case, and break. What are all these? When should you use it instead of if? What are its pros and cons?

Redirects in PHP are simple. I will guide you step by step and show you how to dodge some traps. Finally, we'll learn the nuance between 301 and 302 redirects.

Learn why the "Using $this when not in object context" error happens, and let me show you the only way to fix.

Learn why the "Invalid argument supplied for foreach()" warning happens, and let me show you multiple ways to fix it.