Traits in Laravel Eloquent: 4 Practical Examples

PHP Traits are a powerful feature that allows you to reuse code in multiple classes without repeating yourself.

In this tutorial, I will show 4 examples of how you can use Traits in your Eloquent models.

  • Common Relationships
  • Reusable Methods
  • Extending Traits
  • Adding Listeners from Traits

Let's dive in!


Example 1: Common Relationships

Wanna have the same set of relationships on multiple models? Traits can help.

An example can be found in spatie laravel-permissions package, it has a trait called HasRoles:

/**
* A model may have multiple roles.
*/
public function roles(): BelongsToMany
{
$relation = $this->morphToMany(
config('permission.models.role'),
'model',
config('permission.table_names.model_has_roles'),
config('permission.column_names.model_morph_key'),
app(PermissionRegistrar::class)->pivotRole
);
 
if (! app(PermissionRegistrar::class)->teams) {
return $relation;
}
 
return $relation->wherePivot(app(PermissionRegistrar::class)->teamsKey, getPermissionsTeamId())
->where(function ($q) {
$teamField = config('permission.table_names.roles').'.'.app(PermissionRegistrar::class)->teamsKey;
$q->whereNull($teamField)->orWhere($teamField, getPermissionsTeamId());
});
}

This Trait has a roles() relationship. This means that the following Model, using this Trait, will have the roles() relationship, too:

app/Models/User.php

use Spatie\Permission\Traits\HasRoles;
 
class User extends Authenticatable
{
use HasRoles;
 
// ...
}

And if you need this relationship on another model, you can just add the use HasRoles trait to that model as well. Sounds great, right?


Example 2: Reusable Methods

Another great use case for Traits is to have reusable methods.

Let's take a look at the HasTags trait from spatie laravel-tags package:

public function scopeWithoutTags(
Builder $query,
string | array | ArrayAccess | Tag $tags,
string $type = null
): Builder {
$tags = static::convertToTags($tags, $type);
 
return $query
->whereDoesntHave('tags', function (Builder $query) use ($tags) {
$tagIds = collect($tags)->pluck('id');
 
$query->whereIn('tags.id', $tagIds);
});
}

Reusable scopes will be available on all of your models that use the HasTags trait.

Also, that Trait has methods that attach/detach single/multiple tags:

public function attachTags(array | ArrayAccess | Tag $tags, string $type = null): static
{
$className = static::getTagClassName();
 
$tags = collect($className::findOrCreate($tags, $type));
 
$this->tags()->syncWithoutDetaching($tags->pluck('id')->toArray());
 
return $this;
}
 
public function attachTag(string | Tag $tag, string | null $type = null)
{
return $this->attachTags([$tag], $type);
}
 
public function detachTags(array | ArrayAccess $tags, string | null $type = null): static
{
$tags = static::convertToTags($tags, $type);
 
collect($tags)
->filter()
->each(fn (Tag $tag) => $this->tags()->detach($tag));
 
return $this;
}
 
public function detachTag(string | Tag $tag, string | null $type = null): static
{
return $this->detachTags([$tag], $type);
}

All of those will become available on our Model as soon as we use the Trait:

app/Models/Post.php

use Spatie\Tags\HasTags;
 
class Post extends Model
{
use HasTags;
 
// ...
}

And now we can use those methods in our model:

$post = Post::create(['title' => 'My first post']);
$post->attachTag('laravel');

How cool is that? We just stopped repeating ourselves and made our code more readable and maintainable.

As you can see, package creators often use Traits to add a common repeating functionality into our applications.


Example 3: Extending Traits

In some cases, you want to change how the trait works.

Let's take a look at the InteractsWithMedia trait from the spatie laravel-medialibrary package.

Let's take a look at the getFallbackMediaUrl() method:

public function getFallbackMediaUrl(string $collectionName = 'default', string $conversionName = ''): string
{
$fallbackUrls = optional($this->getMediaCollection($collectionName))->fallbackUrls;
 
if (in_array($conversionName, ['', 'default'], true)) {
return $fallbackUrls['default'] ?? '';
}
 
return $fallbackUrls[$conversionName] ?? $fallbackUrls['default'] ?? '';
}

It seems to be close to what we need but returns an empty string when no media is found. Instead, we want it to return a default image.

To solve that, we can add the method to our Post model, copy-paste its code from the pagkage and override the default behavior:

/app/Models/Post.php

use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
 
class Post extends Model implements HasMedia
{
use InteractsWithMedia;
 
// ...
 
public function getFallbackMediaUrl(string $collectionName = 'default', string $conversionName = ''): string
{
$fallbackUrls = optional($this->getMediaCollection($collectionName))->fallbackUrls;
 
$defaultMediaUrl = "https://laravel.com/img/logotype.min.svg";
 
if (in_array($conversionName, ['', 'default'], true)) {
return $fallbackUrls['default'] ?? $defaultMediaUrl;
}
 
return $fallbackUrls[$conversionName] ?? $fallbackUrls['default'] ?? $defaultMediaUrl;
}
}

Example 4: Adding Listeners from Traits

Another use case for traits might be quickly adding common tracking methods.

For example, if you want a specific action to happen when you are deleting a model. This use case can be found in the previously mentioned spatie laravel-medialibrary

Let's find the bootInteractsWithMedia() method and see what it does:

public static function bootInteractsWithMedia()
{
static::deleting(function (HasMedia $model) {
if ($model->shouldDeletePreservingMedia()) {
return;
}
 
if (in_array(SoftDeletes::class, class_uses_recursive($model))) {
if (! $model->forceDeleting) {
return;
}
}
 
$model->media()->cursor()->each(fn (Media $media) => $media->delete());
});
}

It's looking for the deleting event and then deleting all media attached to the model, unless flags are set to preserve media.

This is a quick way to add a Listener to a Model without having to add it to the Model itself.

These boot methods have naming rules to be autoloaded by Laravel:

  1. Starts with boot
  2. Includes trait name in PascalCase

For example: boot + InteractsWithMedia = bootInteractsWithMedia


Conclusion

Traits are a great way to reuse code and make your code more readable and maintainable. You can find more examples of Traits in our section of code examples.

No comments or questions yet...

Like our articles?

Become a Premium Member for $129/year or $29/month
What else you will get:
  • 59 courses (1056 lessons, total 42 h 44 min)
  • 78 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent Premium Tutorials