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:
- Starts with
boot
- 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...