Lazy eager load relationships without scopes

Published on July 19th, 2019

Today my client asked me to only allow the user to view models in a specific state. The application uses a custom engine for the user interaction and Laravel Nova in the backend. Since the code responsible for the user interaction is quite huge and I didn't want to look up every model query and apply a custom query I figured out adding a global scope would be the easiest way to accomplish this. Unfortunately, we have quite a few jobs in the backend which require to access all models and lazy eager load their relationships through the load() method. The load() method applies all global scopes of a model before it tries to load its relationship, therefore it's not possible to use the load() method for eager loading without scopes - at the first glance at least.

The traditional approach

When searching for a solution for lazy eager load relationships without scopes you will most often find suggestions for adding a second relationship method without the scope you try to avoid.

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

public function allBooks()
{
    return $this->hasMany(Book::class)->withoutGlobalScope(PublishedScope::class);
}

// lazy eager load all books
$author->load('allBooks');

This is a valid solution to the problem. I did not go this way, because I had to take care of multiple models with different relationships to each other and didn't want to pollute my models with several methods for this functionality.

Lazy Eager load without separate method

Turns out like the with() method on the Eloquent Builder class, the load() method does also accept an array with the relationship names as key and a callback as value. So to retrieve all books form an author we simply have to pass the books relationship with a callback which removes the PublishedScope from the query.

$author->load([
  'books' => function ($query) {
    return $query->withoutGlobalScope(PublishedScope::class);
  }
]);

But when we use this approach we pollute our whole codebase with this ugly code. There has to be a better way! In a perfect world, we would be able to call a method on the author which loads all books. Let's image such a method exists. How would you call it? Let's go with loadWithoutScopes(). The method takes the relations as first and scopes as the second parameter. It iterates over all relations and maps them to the key and a callback which takes a $query param and calls withoutGlobalScopes($scopes) on it.

public function loadWithoutScopes($relations, $scopes = null)
{
  $relationsWithoutScopes = collect($relations)->mapWithKeys(function ($relation) use ($scopes) {
    return [
      $relation => function ($query) use ($scopes) {
        return $query->withoutGlobalScopes($scopes);
      }
    ]
  })->toArray();

  return $this->load($relationsWithoutScopes);
}

One trait to rule them all

Now we can take the method loadWithoutScopes(), modify it so it can accept arrays and string as parameters and put it into a LoadsRelationshipsWithoutScops Trait.

trait LoadsRelationshipsWithoutScopes
{
    /**
     * Loads relations without global scopes.
     *
     * @param array|string      $relations
     * @param array|string|null $scopes
     */
    public function loadWithoutScopes($relations, $scopes = null)
    {
        if (is_string($relations)) {
            $relations = [$relations];
        }

        if (is_string($scopes)) {
            $scopes = [$scopes];
        }

        $relationsWithoutScopes = collect($relations)->mapWithKeys(function ($relation, $key) use ($scopes) {
            return [
                $relation => function ($query) use ($scopes) {
                    return $query->withoutGlobalScopes($scopes);
                },
            ];
        })->toArray();

        return $this->load($relationsWithoutScopes);
    }
}

The only step left is to use the LoadsRelationshipsWithoutScopes trait inside all Models you want to apply it and exchange all load() calls with loadWithoutScopes() everywhere where a lazy eager load requires to load relations without scopes.


Thanks for making it so far! I appreciate you taking the time to read through the article. To provide you with higher quality articles I need your feedback on how to write better articles. Did you enjoy the read? Do you have tips on what I could make better? Please tag me on twitter @krishankoenig or write me a mail to krishan.koenig@gmail.com.

Sign up for my newsletter

If you enjoyed this article, sign up for my newsletter to be the first to know when a new article is published.