Maximize your Laravel blog's potential with SEO best practices and reach 10K monthly clicks on Google.
Preview the course for free
Benjamin Crozat New!
Benjamin Crozat The art of crafting web applications

20+ Laravel best practices, tips and tricks to use in 2023

Benjamin Crozat — Updated on
Artboard

Hundreds of developers subscribed to my newsletter.
Join them and enjoy free content about the art of crafting websites!

Powered by

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

Whether you are running Laravel 10, 9 or 8, let’s see in details how I can help you improve any codebase with tons of tips and tricks.

Table of contents

20+ Laravel best practices, tips and tricks to use in 2023

Keep Laravel up to date

Keeping Laravel up to date provides the following benefits:

If Laravel updates scare you, it’s because your codebase isn’t tested. Keep reading to learn more about automated testing.

Keep packages up to date

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

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

Regularly running composer update goes a long way toward a more secure codebase.

Keep your project tested

Keep your project tested

Writing automated tests is a vast and lesser-known topic among developers.

But did you know it’s also the only best practice that ensures reliability?

Here are the benefits of a good test suite:

Ensuring your projects’ reliability with something other than gut instinct will make you an immensely better developer.

Laracasts provides free testing courses to help you get started. One with PHPUnit, the industry standard, and one with Pest, the best testing framework on this planet that modernizes and simplifies testing in PHP.

Stick to the default folder structure

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.

So, why should you stick to Laravel’s default project structure?

  1. Convenience. Laravel’s default way of doing things is documented. When you come back on a project weeks or months later, you will thank your past self for this.
  2. Working with team mates is considerably easier. They know Laravel, just like you. Use this common knowledge to help the project move forward instead of reinventing the wheel every time.

When should you not stick to the defaults?

When the size of your project actually requires to do things differently.

The most popular custom project structure for Laravel is the domain-driven architecture.

Stick to the default folder structure

Use custom form requests

The main reason to use custom form requests are:

  1. Reusing validation across multiple controllers;
  2. Offloading code from bloated controllers;

Creating custom form requests is as simple as running this Artisan command:

php artisan make:request StorePostRequest

Then, in your controller, just type-hint it:

use App\Http\Requests\StorePostRequest;
 
class PostController
{
function store(StorePostRequest $request)
{
$validated = $request->validated();
 
Post::create($validated);
 
//
}
}

Custom requests can also be used for authorization, if you feel like Policies are overkill.

Use single action controllers

Sometimes, despite following all the good practices, your controllers become too big.

Laravel provides a way to help you fix this: Single Action Controllers.

Instead of containing multiple actions, like Resource Controllers, Single Action Controllers contain just one.

To create one, use the php artisan make:controller ShowPostController --invokable command.

This will create a controller with only one action called __invoke (learn more about the __invoke magic method).

Then, in your routes, you can do this instead:

-use App\Http\Controllers\PostController;
+use App\Http\Controllers\ShowPostController;
 
-Route::get('/posts/{post}', [PostController::class, 'show']);
+Route::get('/posts/{post}', ShowPostController::class);

This is a subjective best practice and it’s up to you to decide whether you want to use single action controllers or not.

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.

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 them 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 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();
});
}
}

Use Eloquent’s naming conventions for table names

Laravel’s naming conventions for tables is easy and one best practice that will simplify your team’s life.

First, let me remind you that the framework does it all for you when you’re using Artisan commands like php artisan make:model Post --migration --factory.

For whatever reason, if you can’t use those commands, here’s an overview:

Read the documentation for more information.

Prevent N+1 issues with eager loading

I’ve talked about so many good practices, but it’s far to be over!

Ever heard about N+1 problems? Eager loading is a great solution to avoid them.

N+1 problem with Eloquent

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;
}
}

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 one of them 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.

And, to easily manage your jobs through a user interface, Laravel Horizon is what you should use.

Use queues for even longer running tasks

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 Illuminate\Foundation\Testing\LazilyRefreshDatabase 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 PostFactory command.

Using them, you can create all the resources you 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.

Then, why are you running your tests using SQLite?

Use MySQL instead. You will be able to catch MySQL-related bugs in your local environement instead of waiting for things to go wrong in production.

Same for caching. If you are running Redis in production, why are you testing using the array driver?

I’m convinced reliability and accuracy are more important than speed in this context.

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 Illuminate\Foundation\Testing\DatabaseTransactions trait in your tests/TestCase.php.

Don’t waste API calls, use mocks

In Laravel, mocks can be used to avoid wasting API calls while testing and being hit with rate limit errors.

Let’s say we are working on a project using Twitter’s API.

In our container, we have a Client class used to call it.

While running our test suite, we want to avoid unecessary calls to the real thing and the best way to do it is to swap our client in the container by a mock.

$mock = $this->mock(Client::class);
 
$mock
->shouldReceive('getTweet')
->with('Some tweet ID')
->andReturn([
'data' => [
'author_id' => '2244994945',
'created_at' => '2022-12-11T10:00:55.000Z',
'id' => '1228393702244134912',
'edit_history_tweet_ids' => ['1228393702244134912'],
'text' => 'This is a tweet',
],
]);

Learn all about mocking on Laravel’s documentation.

Prevent stray HTTP requests

If you want to make sure that all HTTP requests made during your tests are fake, you can use the preventStrayRequests() method.

It will cause an exception to be thrown if any HTTP requests that do not have a corresponding fake response is executed.

You can use this method in an individual test or for your entire test suite.

Http::preventStrayRequests();

Don’t track your .env file

This best practice 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 in the history you tracked 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

Your CSS and JavaScript are generated using originals in resources/css and resource/js.

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

Especially for people still using Laravel Mix, I recommend to stop tracking them.

It’s quite annoying that every time you change something, a new public/css/app.css or public/js/app.js is generated and need to be commited.

It only takes two lines in your .gitignore to stop this:

public/css
public/js
Recommended

Nailing a Laravel job interview can be a daunting task, but with the right preparation and mindset, you can set yourself up for success.

Laravel 10 has been released on February 14, 2023. Let's dive into every relevant new feature and change.

Migrations are essential in any Laravel app using a database. I will tell you what they are, why you should use them and how you can generate them.

I show you how to upgrade your Laravel 9 project to version 10 and help you decide whether the return on investment is worth it.

Here's a case study for my blog in the programming niche, where I share everything I did to increase clicks by a huge amount since the beginning.

I show you how to upgrade your Laravel 8 project to version 9 and help you decide whether the return on investment is worth it.

Store and manage files on Dropbox and use it to back up your Laravel app automatically. Compatible with PHP 8.1+ and Laravel 9+.

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

Start leveraging the power of AI today. It enables developers to do incredible things, and many startups build products around it.

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

Powered by