🥖🇫🇷🏴 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. 🐰
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
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();
});
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(),
];
}
Wonderful. So now, you can create various random bunnies using the factory:
Bunny::factory()->count(10)->create();
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));
}
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()
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();
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.
}
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));
}
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);
}
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.
Top comments (4)
Hi, can you make a benchmark for efficiency in using scope?
why tho?
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.
Thanks for your kind comment.
This would be a great idea for a contribution ! :)