DEV Community

Cover image for Organize your models scopes better in Laravel
Rachid
Rachid

Posted on

Organize your models scopes better in Laravel

🥖🇫🇷🏴󠁣󠁡󠁱󠁣󠁿 Si vous préférez le français, cet article est disponible dans la langue de Molière sur mon blogue.

Intro

Sometimes I find my models cluttered with a lot of things: relationships, attributes casting, scopes, custom methods, etc.

When this happens, one thing I like to do is move my scopes in a dedicated builder. This post will show you how I do that.

We are working with bunnies because cats are all over the place already and because of the pressure I get from Coton the Tige. 🐰

Picture of Coton The Tige

Setup

Let's spin up our artisan command to create the model with the migration flag (-m) and the factory flag (-f):

php artisan make:model Bunny -mf
Enter fullscreen mode Exit fullscreen mode

Now let's define the data structure of our fleecy table (gist):

Schema::create('bunnies', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->tinyInteger('fluffiness');
    $table->date('birth_date');
    $table->timestamps();
});
Enter fullscreen mode Exit fullscreen mode

Great, now let's fill in the definition of our factory class (gist):

public function definition(): array
{
    return [
        'name' => $this->faker->userName,
        'fluffiness' => $this->faker->numberBetween(1, 100),
        'birth_date' => $this->faker->date(),
    ];
}
Enter fullscreen mode Exit fullscreen mode

Wonderful. So now, you can create various random bunnies using the factory:

Bunny::factory()->count(10)->create();
Enter fullscreen mode Exit fullscreen mode

Using scopes

Scopes are a great way to add filters (ie. query constraints) that can be easily reused all across your application.

For instance we could have a scope that says:

I want only the old bunnies (those that are 5 years old and up)

For that I can create a method in the Bunny model that goes like this (gist):

public function scopeOld(Builder $query): Builder
{
    return $query->where('birth_date', '<=', now()->subYears(5));
}
Enter fullscreen mode Exit fullscreen mode

A scope method name must start with the word scope, this is how Eloquent can find it. (More details about this in the Illuminate\Database\Eloquent\Builder class, see API definition here)

That way I can grab all my old bunnies simply by executing this query:

Bunny::old()->get()
Enter fullscreen mode Exit fullscreen mode

You can check this gist to get the Test Case I used while writing this velvety blog post.

This is all fun and games until you start getting a lot of stuff in that model and it goes from fluffy to shaggy. We don't want that.

This is why I like to create custom Builder, but before that, let's talk briefly about the Eloquent Builder.

The Eloquent Builder

When you do something like this:

Bunny::old();
Bunny::where('name', 'Arnaboun');
Bunny::latest();
Enter fullscreen mode Exit fullscreen mode

You're not calling the Bunny model but the underlying Eloquent Builder class. This is the one in charge of building the query that will hit the database.

So when you're creating scopes for your fluffy models, what you are actually doing is provide new shortcuts to query your database. So, in my opinion, those scopes have a better spot right inside the Builder class.

Let's see how to make this a reality.

Creating a custom Builder

To create a custom builder, you need to create the appropriate class and it should extend Illuminate\Database\Eloquent\Builder.
A custom builder should concern one, and only one, model.

Feel free to put this class wherever it makes more sense to you, I usually create a Builders folder inside app.

This is how it looks:

<?php

namespace App\Builders;

use Illuminate\Database\Eloquent\Builder;

class BunnyBuilder extends Builder
{
    // hi mom.
}
Enter fullscreen mode Exit fullscreen mode

Now I can migrate the scopes, go check the gist so that this post isn't too much scrollable.
Here is a sneak peek onto our previous scope:

public function old(): self
{
    return $this->where('birth_date', '<=', now()->subYears(5));
}
Enter fullscreen mode Exit fullscreen mode

You'll notice two things:

  • We don't need the scope in our method's name anymore
  • We don't need to inject the Eloquent Builder because its now our context. ~ yaii, computer!~

Using our custom builder

Okay great, we have a custom builder but now how can we tell our model to use it ?

Laravel offers us a method that can create an instance of a custom builder (see its API definition). So we can simply drop this into our model:

public function newEloquentBuilder($query)
{
    return new BunnyBuilder($query);
}
Enter fullscreen mode Exit fullscreen mode

And now, if we re-run our tests, it will still works meaning we didn't break anything. Our scopes can now live into their new realm and free up a bit the models responsibilities.


Thanks for reading me, this is my first attempt at writing in dev.to in english. Let me know how I did and how I can improve and progress and in the meantime, I hope you have a wonderful day.

Discussion (5)

Collapse
fadlisaad profile image
Fadli Saad

Hi, can you make a benchmark for efficiency in using scope?

Collapse
bdelespierre profile image
Benjamin Delespierre

why tho?

Collapse
rachids profile image
Rachid Author

Hello Fadli, I ran a few test with clockwork on the scopes inside the model vs. inside a builder and there are no perceptible differences.

Collapse
mlouis profile image
MLouis

Great article! In my opinion, it should be included into Laravel, with a command like php artisan make:builder ModelBuilder 👍

Collapse
rachids profile image
Rachid Author

Thanks for your kind comment.

This would be a great idea for a contribution ! :)