Scopes in Eloquent are a great way to define common sets of query constraints that you may easily re-use throughout your application.

They are usually defined inside the Model class:

namespace App\Models;

use Illuminate\Database\Eloquent\Builder
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function scopePopular(Builder $query): Builder
    {
        return $query->where('votes', '>', 100);
    }

    public function scopeActive(Builder $query): Builder
    {
        return $query->where('active', 1);
    }
}

Once defined, you can use them like this:

use App\Models\User;

$users = User::popular()->active()->orderBy('created_at')->get();

One drawback is that sooner than later, you will get a lot of scopes in your model which clutters the code. It would be nice to have them in a separate class.

To achieve this, we'll create a custom Eloquent 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.

In our example, the builder will be defined like this:

namespace App\Builders;

use Illuminate\Database\Eloquent\Builder;

class UserBuilder extends Builder
{
    public function popular(): self
    {
        return $this->where('votes', '>', 100);
    }

    public function active(): self
    {
        return $this->where('active', 1);
    }
}

What you can see is that you no longer need the scope prefix neither do you need the $query parameter as the function argument.

To use this in your model, you need to override the newEloquentBuilder method:

namespace App\Models;

use Illuminate\Database\Eloquent\Builder
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function newEloquentBuilder($query)
    {
        return new UserBuilder($query);
    }
}

Usage is still exactly the same:

use App\Models\User;

$users = User::popular()->active()->orderBy('created_at')->get();

Thanks to this article for the idea.