Refactoring Nested Queries in Laravel With Dot Notation And Arrow Functions

Arie Visser • April 24, 2020

laravel php database

Recently I stumbled upon a nested query in a Laravel project.

It was written like this:

User::whereHas('orders', function ($query) use ($payment) {
    $query->whereHas('products', function ($query) use ($payment) {
        $query->whereHas('payments', function ($query) use ($payment) {
            $query->where('id', $payment->id);
        });
    });
})->get();

You might have seen a comparable nested query in the code you had to work on, and you probably agree that they are quite hard to read. Very often, nested queries arise when a new feature is requested that was not taken into consideration during the initial design of the data model. The above statement was created because a notification had to be sent to all users that were related to a certain payment.

In some cases, nested queries can be avoided by improving the design of the data model. In this guide, I would just like to show some possible improvements for the readability of such a nested query.

"Dot" notation

The first thing you can do, is to add "dot" notation to the whereHas method:

User::whereHas('orders.products.payments', function ($query) use ($payment) {
    $query->where('id', $payment->id);
})->get();

In this way, Eloquent will be able to interpret the nested relationship. This reduces the amount of code by four lines!

Arrow function

A second enhancement is to add arrow function notation (short closure), introduced in PHP 7.4:

User::whereHas(
    'orders.products.payments', 
    fn ($query) => $query->where('id', $payment->id)
)->get();

Since you can access values from the parent scope when using short closures (in this case $payment), we also got rid of the use keyword.

Only one expression is allowed in a short closure.

Change the where-statement

We also can add the column name directly to the where-statement:

User::whereHas(
    'orders.products.payments', 
    fn ($query) => $query->whereId($payment->id)
)->get();

As a result, the whole nested query could fit on one line.

Code styling

The code style of the query still can be enhanced a bit, by adding the query() method for alignment, and writing each statement on its own line.

I also abbreviated $query as $q, to make everything fit within 80 characters, and lead the visual focus to the statements itself:

User::query()
    ->whereHas('orders.products.payments', fn ($q) => $q->whereId($payment->id))
    ->get();

In this way it is very easy to understand what the code is doing.

Of course, there is a lot of room for dividing opinions in subjects like these. Please consider the examples as suggestions.

Read more about arrow functions in PHP.

The idea to use query() for alignment, was taken from here.