Organize your models scopes better in Laravel
Tags: laravel, scope, model, architecture
Published: 2021-12-08 22:36:11

> 🥖🇫🇷🏴󠁣󠁡󠁱󠁣󠁿 Si vous préférez le français, cet article est disponible dans la langue de Molière sur [mon blogue](https://www.rachids.ca/blog/mieux-organiser-les-scopes-de-nos-models-dans-laravel).

## 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](https://imgur.com/AdsUQIs.png?2 "Coton The Tige, pressuring his hoomans")

## Setup
Let's spin up our artisan command to create the model with the migration flag _(`-m`)_ and the factory flag _(`-f`)_:
```bash
php artisan make:model Bunny -mf
```

Now let's define the data structure of our fleecy table ([gist](https://gist.github.com/rachids/7b8026a8045f245ace686319fb81ae52)):
```php
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](https://gist.github.com/rachids/c0d4a1d8b3aaf2ac3d61305838ee4426)):
```php
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:

```php
Bunny::factory()->count(10)->create();
```

## Using scopes
[Scopes](https://laravel.com/docs/8.x/eloquent#local-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](https://gist.github.com/rachids/f7497ccda4aeed3c89c627f0545ab952)):
```php
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](https://laravel.com/api/8.x/Illuminate/Database/Eloquent/Builder.html#method_hasNamedScope))_

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

```php
Bunny::old()->get()
```

You can check this [gist](https://gist.github.com/rachids/ab79bcd6d22225763d3f22368d4ad2bc) 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:
```php
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
<?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](https://gist.github.com/rachids/1c9c42636ce6546c6303eeafa88869c4) so that this post isn't too much scrollable. 
Here is a sneak peek onto our previous scope:

```php
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](https://laravel.com/api/8.x/Illuminate/Database/Eloquent/Model.html#method_newEloquentBuilder))_. So we can simply drop this into our model:

```php
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.