Using the Laravel Active Trait

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);
    }
}