Sometimes the findOrFail() (or any other default) method is not enough and you need to have a method that applies to multiple models at the same time. This article describes how to achieve this. ?
Let’s grab a real simple example in this guide. Let’s say you have a News model and a Page model, both have the slug column in the database.
Sure, you could do something like this:
News::where('slug', 'my-slug')->firstOrFail();
But wouldn’t it be way nicer to have something like:
News::findBySlugOrFail('my-slug');
This is where extending the Laravel query builder comes in handy.
Start by creating a new file: app/Builder/MyBuilder.php
(You can name this anything you like to be more specific, like AppBuilder.php or something like that. Just make sure you name the class inside the same as the file name for the namespace to kick in properly ?)
With these contents:
<?php
namespace App\Builder;
use Illuminate\Database\Eloquent\Builder;
class MyBuilder extends Builder
{
}
Before we continue adding methods inside this builder, lets prepare our models to start using this class instead of the default Model from Eloquent.
If you where to have a model like News (or Page) it would look somewhat like this:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class News extends Model
{
protected $fillable = [
'slug',
'title',
'content'
];
}
We will have to start telling this model to use the newer query builder class. We do this like so:
<?php
namespace App;
use App\Builder\MyBuilder;
class News extends MyBuilder
{
protected $fillable = [
'slug',
'title',
'content'
];
}
This will still work like before, everything is available like you’re used too. We are now going to add new method to the MyBuilder.php class.
So lets say, we want to have a findBySlug() method:
<?php
namespace App\Builder;
use Illuminate\Database\Eloquent\Builder;
class MyBuilder extends Builder
{
public function findBySlug($slug, $columns = ['*'])
{
return $this->where('slug', $slug)->first($columns);
}
}
This will now allow you to do queries like this:
News::findBySlug('my-slug');
(And if you want, you can even pass in an array with columns you want to select)
You can go absolutely wild with this, lets extend this even further:
<?php
namespace App\Builder;
use Illuminate\Database\Eloquent\Builder;
class MyBuilder extends Builder
{
public function findBySlug($slug, $columns = ['*'])
{
return $this->where('slug', $slug)->first($columns);
}
public function findBySlugOrFail($slug, $columns = ['*'])
{
return $this->where('slug', $slug)->firstOrFail($columns);
}
}
This will make it possible to get a 404 if the first result is never found:
News::findBySlugOrFail('my-slug');
Basically each method from the eloquent class is available in the builder class; whereIn, whereHas, where, whereBetween, with, load etc etc etc..
Another example
I have a project where the slugs are stored inside a separate table, and polymorphed to its models.
To achieve the same thing as above, I will have to query on a relationship inside a model, and this is how I did that:
/**
* Find a model by its slug.
*/
public function findBySlug($slug, $columns = ['*'])
{
return $this->whereHas('slug', function ($query) use ($slug) {
return $query->where('slug', $slug);
})->first($columns);
}
/**
* Find a model by its primary slug or throw an exception.
*/
public function findBySlugOrFail($slug, $columns = ['*'])
{
return $this->whereHas('slug', function ($query) use ($slug) {
return $query->where('slug', $slug);
})->firstOrFail($columns);
}
As you can see, there are many methods to write for your own logic to clean up controllers and your code at the same time.
And thats it! You can now start writing your own query methods inside your builder. Of course, you can also create multiple builders for specific use cases to separate code and clean it up more. ?