Laravel Active Trait

When adding functionality that might be applied in multiple places, you might consider a way to reuse the code. You can either make all your classes extend a base class using inheritance or you can create a trait;

Traits are a mechanism for code reuse in single inheritance languages such as PHP. A Trait is intended to reduce some limitations of single inheritance by enabling a developer to reuse sets of methods freely in several independent classes living in different class hierarchies.

I have used traits for common functionality applied to Laravel Eloquent models. Often there are models which have an active flag in the database and should only appear on the website when this is true. This post documents how I build flexible traits for this use case.

Setting up the trait

Laravel calls traits by a language-agnostic name of “concerns” and organises them inside concerns folders. I have been using traits as the naming convention, but you can organise these how you best see fit.

We want to be able to check whether an object is "active" and find all the objects (in the database) that are active. First of all, we create our Active trait class in the appropriate folder. I am using the long-established accessor behaviour using the get{Attribute}Attribute convention. The new Attribute-based accessor sytnax in Laravel 9 will also work.

And secondly, we can add a basic scope. This checks whether the active field in the database is equal to 1.

This is the basis of the trait but doesn't do anything useful at the moment. The attribute is just hardcoded to return false, but the scope does act correctly.

<?php

namespace App\Concerns;

trait Active
{
    public function getActiveAttribute(): bool
    {
        return false;
    }

	public function scopeActive(Builder $query): Builder
    {
        return $query->where('active', '=', 1);
    }
}

Next, we can add a bit of flexibility. Similar to how Laravel has a getKeyName() method, which you can configure in each model if needed, we can add a getActiveColumnName() and set a sensible default. This allows you to configure the "active" database column in a model if it is different to the default. We can make this method protected because only our trait is concerned with the method, but this still allows our model to override it.

public function scopeActive(Builder $query): Builder
{
	return $query->where($this->getActiveColumnName(), '=', 1);
}

protected function getActiveColumnName(): string
{
	return 'status';
}

Enums arrived in PHP 8.1 and using these we can replace the hardcoded 1 with a more descriptive value, for example, we can create an enum for the status;

enum Status
{
    case DRAFT = 0;
    case PUBLISHED = 1;
}

If you aren't using PHP 8.1 yet, then I have been using enums in Laravel for a while using an easy-to-install package. It works in a similar way, but you use classes inheriting the package functionality instead of the native enum syntax.

Now we can update our activeScope to use this new enum;

use App\Enums\Status;

public function scopeActive(Builder $query): Builder
{
	return $query->where($this->getActiveColumnName(), '=', Status::PUBLISHED);
}

The final piece in the trait is getting the correct value for the active accessor. We need to get the attribute for the appropriate column name. We are adding this trait to our Eloquent models, which have the getAttribute() method for accessing model data. We can make this method a requirement of our trait by creating an abstract function declaration. Finally, we use that Laravel accessor method in combination with our getActiveColumnName() method for the appropriate column name.

abstract public function getAttribute(string $key): mixed;

public function getActiveAttribute(): bool
{
	return $this->getAttribute($this->getActiveColumnName());
}

The final trait should look like this;

<?php

namespace App\Concerns;

use App\Enums\Status;
use Illuminate\Database\Eloquent\Builder;

trait Active
{
    abstract public function getAttribute(string $key): mixed;

    public function getActiveAttribute(): bool
    {
        return $this->getAttribute($this->getActiveColumnName());
    }

    public function scopeActive(Builder $query): Builder
    {
        return $query->where($this->getActiveColumnName(), '=', Status::PUBLISHED);
    }

    protected function getActiveColumnName(): string
    {
        return 'status';
    }
}

In another post, I will go into the details of how you use the trait in your model. How the flexibility of this trait can be used to configure the column name and how to extend the trait for more complex scopes.