Laravel advanced: custom query builders
Sometimes, developers face challenges in maintaining clean models or get frustrated with the terrible code hinting that occurs when using scopes.
Local scopes
Below is a typical example of using scopes.
class Post extends Model {
public function scopeRunningBetween(Builder $builder, Carbon $start, Carbon $end)
{
return $builder->whereBetween(('published_at', [$start, $end]);
}
// ... And a few more scopes
}
Post::query()
->runningBetween(Carbon::startOfMonth(), Carbon::endOfMonth())
->where('status', 'published');
While the example above works, it gives horrible code completion when using vscode or PhpStorm.
- The model itself will become bloated
- Editors like PhpStorm won’t detect breaking name changes, when renaming the method
scopeRunningBetween()
- Pressing
Remove element ‘scopeRunningBetween’
could possible cause breaking changes - Once you use the
scopeRunningBetween()
method, it returns zero for auto-completion. This means that there are no suggestions or options available for further completion; type-hinting, other available methods likewhere()

Query builder
Instead of defining local scopes, you could move all logic in a separate PHP-class that extends the Eloquent Builder class.
Here I am going to give introduction how to create custom query builders.
<?php
use Illuminate\Database\Eloquent\Builder;
class ArticleQueryBuilder extends Builder {
public function runningBetween(Carbon $start, Carbon $end)
{
return $this->whereBetween(('published_at', [$start, $end]);
}
}
Every-time you have a model and you use query()
its returns and instance of Builder.
<?php
class Article extends Model {}
/** @var Builder $query */
$query = Article::query();
$query->where('user_id', auth()->id())->get();
But instead want to override the method newEloquentBuilder
and all your local scopes to ArticleQueryBuilder.php
.
class ArticleQueryBuilder extends Builder {
public function user(int $userId): ArticleQueryBuilder {
return $this->where('user_id', $userId);
}
}
class Article extends Model {
public function newEloquentBuilder($query): ArticleQueryBuilder
{
return new ArticleQueryBuilder($query);
}
}
$query = Article::query()->user(auth()->id())->get();
So next time when you use query()
you get an instance of ArticleQueryBuilder
.
From here on I am going to add tips and tricks to improve code-completion or give an alternative solution such as tappable scopes.
Poor code-completion
You have successfully relocated all your local scopes to enhance maintainability. However, you are still seeking ways to enhance code-hinting.
Article::query()
still thinks its returning the eloquent builder- vscode or PhpStorm won’t suggest the method
user()
orrunningBetween()
- Your editor will detect the correct type-hinting of a parameter

Improve auto-completion
You won’t have access to proper auto-completion even after successfully implementing your custom query builder. To solve this problem need to add PHP-docs to override the code-hinting suggested by query()
.
/**
* @method static ArticleQueryBuilder query()
*/
class Article extends Model {
use SoftDeletes;
public function newEloquentBuilder($query): ArticleQueryBuilder
{
return new ArticleQueryBuilder($query);
}
}
Your editor will detect the methods and make it easier to find usages when renaming a method.

Improve retrieving soft-deleted records
Sometime, the code-completion can still encounter problems when using methods such as withTrashed()
when you use the the trait useSoftDeletes
.

Now instead of adding the PHP-doc on the model itself, you add one on the PHP-class ArticleQueryBuilder
.
/**
* @method static ArticleQueryBuilder withTrashed
*/
class ArticleQueryBuilder extends Builder {
public function user(int $userId): ArticleQueryBuilder {
return $this->where('user_id', $userId);
}
}
This will solve the problem when using methods related to soft-deletes.
Alternative tappable scopes
There is also an alternative solution or one you can combine with custom query builders.
<?php
class IsHungryTap {
public function __invoke(Builder $builder): void
{
// https://ownyourpet.com/why-are-guinea-pigs-always-hungry/
$builder->where("last_meal_at", '>', now()->subMinutes(15));
}
}
$query = GuineaPig->query()->tap(new IsHungryTap())->get();
https://medium.com/@bastiaandewaele/laravel-advanced-tappable-scopes-afd75187182c