Laravel advanced: custom query builders

Bastiaan Dewaele

--

Photo by Ben Griffiths

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 like where()

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() or runningBetween()
  • 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

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Bastiaan Dewaele
Bastiaan Dewaele

Written by Bastiaan Dewaele

Senior back-end developer in Ghent who likes writing sometimes weird / creative solutions to a specific problem.

No responses yet

Write a response