In a previous blog post, I wrote how I build traits for using in Laravel. This post discusses how you can use the trait in your Eloquent models. It covers how you actually use them, using the flexibility we built in, and extending trait methods. Also discusses improving the trait with a global scope and adding a contract to your models.
Using the Trait
To use the trait, you add a use
statement to your Eloquent model. This effectively copies all the methods from the trait into your model.
<?php
namespace App\Models;
use App\Concerns\Active;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use Active;
}
Now the Post
model has access to the active
attribute and the scope can be applied to the query builder for that model.
$post = Post::query()->active()->first();
$post->active;
Overriding the Column Name
Because the trait was built with flexibilty in mind, it has a getActiveColumnName()
method. If the active column is different on a specific model, we can easily override it while keeping the rest of the code the same. For example, instead of the model using the status
column, it has an active
column. We can override this using the method and everything else will work the same as before.
<?php
namespace App\Models;
use App\Concerns\Active;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use Active;
protected function getActiveColumnName(): string
{
return 'active';
}
}
Extending the Scope
If your model has more rules on when it is considered active, then you can still use the trait and extend the scope. You need to update the use
statement to map the trait method you might want to use, to something different. This allows the class to override the method while still giving the ability to call the trait-defined method.
<?php
namespace App\Models;
use App\Concerns\Active;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use Active {
scopeActive as parentScopeActive;
}
public function scopeActive(Builder $query): Builder
{
return $this->parentScopeActive($query)->where(...);
}
}
Booting the Scope
In most applications, you are only concerned about active data. Instead of manually adding the active
scope to every query you build, you can apply global scopes. Instead of applying this to the model, we can apply it directly to the trait.
Using the bootable eloquent traits functionality that Laravel provides, the following code will make sure every model that uses the active trait will also only return active items.
public static function bootActive(): void
{
static::addGlobalScope('active', fn (Builder $builder) => $builder->active());
}
This means when you use any query builder methods, only active models are returned. The following queries will include the active scope by default.
Post::all();
Post::query()->where(...)->paginate();
If you need to show models without this scope, Laravel makes it easy;
Post::query()->withoutGlobalScope('active')->get();
Adding a Contract
You can add an interface or contract to your model. This helps with auto-completion, makes the code more concrete and allows you to type hint based on contracts. We can create an interface
which includes the public
method from our trait. This can then be applied to all the Eloquent models which use the trait.
<?php
namespace App\Contracts;
use Illuminate\Database\Eloquent\Builder;
interface IsActive
{
public function getActiveAttribute(): bool;
public function scopeActive(Builder $query): Builder;
}
This can then be added to your model using the implements
syntax.
<?php
namespace App\Models\Post;
use App\Concerns\Active;
use App\Contracts\IsActive;
use Illuminate\Database\Eloquent\Model;
class Post extends Model implements IsActive
{
use Active;
}
You can also add the contract on models that don't use the trait. If your class doesn't behave in the same way as the trait conventions, it doesn't make sense to use it. However, you still might want to make the class follow the same interface contract. Adding the contract will force you to add any missing methods.
<?php
namespace App\Models\Post;
use App\Contracts\IsActive;
use Illuminate\Database\Eloquent\Model;
class Post extends Model implements IsActive
{
public function getActiveAttribute(): bool
{
return ! $this->getAttribute('deleted');
}
public function scopeActive(Builder $query): Builder
{
return $query->where('deleted', '=', 0);
}
}