GitHub LinkedIn
Profile Portfolio Blog

Jan 29, 2025

Laravel's magic and its pitfalls

Laravel Eloquent Optimisation

Laravel's magic is everywhere and it can help you reduce the size of your codebase by way of "shortcutting" the code but is it actually the most efficient way of writing code? The answer is yes; but also no. Eloquent alone is full of tricks that can make your code short and sweet but, sometimes, it comes at a cost.

Check out this short video that goes a bit more in depth about Laravel's magic.

A classic example of Laravel's magic

Imagine you have a website to manage a book database. You'll likely have models for authors, books, and genres. To keep things simple, let's assume one book can only have one author but one author can have multiple books. You'll then define the following relationships in both models.

// Models/Author.php

public function books(): HasMany
{
    return $this->hasMany(Book::class);
}
// Models/Book.php

public function author(): BelongsTo
{
    return $this->belongsTo(Author::class);
}

Already here you have a bit of magic. I've purposely omitted the foreign keys when defining these relationships since Eloquent will automatically determine the proper foreign key column by taking the "snake case" name of the parent model and suffixing it with _id. So, in this example, Eloquent will assume the foreign key column on the Book model is author_id.

While the code looks cleaner without referencing the foreign keys, it may lead a new developer to think that you don't need them, especially if they're unfamiliar with the way Laravel works. And in the future, if a migration for a new model happens to have a foreign key with a non-standard name, defining relationships like the examples above will not work and the new developer will be left scratching their head.

A common mistake when leveraging Eloquent's magic in relationships

Another great example, and perhaps more likely to trick even more experienced developers, is Eloquent's dynamic relationship properties, which allow you to access relationship methods as if they were defined as properties on the model.

use App\Models\Author;

$comments = Author::find(1)->books;

In the example above, by calling the books property, you're actually calling the books relationship method defined in the Author model and retrieving a collection - of all books related to that author, in this case. This gets easier to visualise when you compare it with the following code that does the exact same thing.

use App\Models\Author;

$comments = Author::find(1)->books()->get();

The N+1 problem

Dynamic relationship properties can be great at keeping things simple but whenever you use them, odds are you're working with Eloquent Collections instead of a single model; and if you simply stick to the code above, you'll run into trouble.

Take the following code as an example where we try to display a list of all books and their respective authors.

use App\Models\Book;

$books = Book::all();

foreach ($books as $book) {
    echo $book->title . '-' . $book->author->name;
}

The thing about this block of code is that it will work - and for many, that's enough. What is not immediately obvious here is that, if you have, for instance, 1000 books in your database, this code will execute 1001 queries to the database - the initial query where you get all the books, plus an additional query, for each loop, to get a book's author - and that doesn't sound very efficient, does it? So how do we fix it?

By eager loading all necessary relations beforehand, you can fetch the exact same data in one single query, thus saving a lot of time. In this case, you use the with method to "append" the respective Author to each book and this data will be included in the collection generated by Eloquent.

use App\Models\Book;

$books = Book::with('author')->get();

Pro tip: You can use the dot notation to retrieve nested relationships in order to keep everything in the same query.

Preventing lazy loading

Ironically, a best practice that helps to mitigate this magic pitfall from Laravel is... more magic! You can prevent this from happening anywhere in your code by simply adding Model::preventLazyLoading(); to the boot method in your AppServiceProvider class. This will throw an exception every time you try to access a model relationship that hasn't been loaded and that will make it a lot easier to find where you can optimise your website. I've wrapped this in a condition to make sure it only runs locally because otherwise, if you already have a big codebase running on a production environment, odds are you are lazy loading somewhere and a user will trigger it.

// app/Providers/AppServiceProvider.php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\App;


if (App::environment('local')) {
    Model::preventLazyLoading();
}

© Antonio Lima, 2025.