Using Laravel's Bootable Eloquent Traits
In this blog post we’ll take a look at using PHP traits with Laravel’s Eloquent models, hooking them up with events and making them configurable.
Traits are a useful means of writing DRY code. They provide a way of horizontally sharing logic between classes. Any properties or methods of a trait become instantly available to any class we attach the trait to as if they were properties or methods of that class. They also allow us to pick ‘n’ mix behaviours for classes in a way that class inheritance doesn’t.
When using traits with Eloquent models, Laravel has a neat trick that boots the trait allowing us to hook into the Eloquent events. Laravel’s own SoftDeletes
trait uses this bootable feature to add a global scope that allows us to soft delete a model.
To take a look at how we can use traits with our Eloquent models we’ll consider a real world example and look under the hood to see how Laravel enables us to boot traits.
For our example we’ll create a basic trait for adding a sluggable behaviour to models that generates SEO friendly URL slugs.
Before we continue, the following example is just to demonstrate a practical use of traits with Laravel. If you are here wanting to incorporate a sluggable behaviour into your own app I’d suggest installing one of the many existing packages out there that will do this for you. For example, Colin Viebrock’s Eloquent Sluggable There’s no point re-inventing the wheel!
A Simple Trait Example
So let’s get started by defining a Sluggable
trait with a simple method for generating a slug from a string.
<?php namespace App\Traits;
trait Sluggable { public function generateSlug($string) { return strtolower(preg_replace( ['/[^\w\s]+/', '/\s+/'], ['', '-'], $string )); } }
We can now attach this trait to any class that we want to have the generateSlug
method. For example, let’s say we’ve got an Article
model that we want to generate slugs for. We attach traits to classes using the keyword use
inside a class (not to be confused with namespace importing/aliasing).
<?php namespace App;
use Illuminate\Database\Eloquent\Model; use App\Traits\Sluggable;
class Article extends Model { use Sluggable; // Attach the Sluggable trait to the model }
The Article
model now has the method generateSlug
, so we can use it to generate a slug everytime we save an article using the saving
event. So let’s add this to the model’s boot
method.
<?php namespace App;
use Illuminate\Database\Eloquent\Model; use App\Traits\Sluggable;
class Article extends Model { use Sluggable; // Attach the Sluggable trait to the model
public static function boot() { static::saving(function ($model) { // Remember that $model here is an instance of Article $model->slug = $model->generateSlug($model->title); }); } }
Now the article’s slug property is being set on save. However, we don’t really want to have to add all this code to each model we add the trait to in order to hook into the saving
event. Thankfully Laravel has a solution for this, we can boot Eloquent traits!
Booting Eloquent Traits
In Laravel, if we use a trait with an Eloquent model we get some extra magic thrown in. You can boot your traits by adding a boot[TraitName]
method to it. So for our example Sluggable
trait the method would be named bootSluggable
. In this method we can add the code we already have for generating the slug on save that we previously wrote directly in our Article
model.
<?php trait Sluggable { public static function bootSluggable() { static::saving(function ($model) { $model->slug = $model->generateSlug($model->title); }); }
public function generateSlug($string) { return strtolower(preg_replace( ['/[^\w\s]+/', '/\s+/'], ['', '-'], $string )); } }
The trait’s boot method works just like an Eloquent model’s boot
method. So you can hook in to any of the Eloquent events from here. The boot method of each associated trait will get called at the same time as the model’s boot
method.
Under the hood Laravel is using the PHP method class_uses
which returns an array containing the names of all the traits of the given class. This array can then be used to see if the trait has added the boot[TraitName]
method to the class and if the relevant method exists it calls it.
If you want to look at Laravel’s sourcecode to see what’s going on check out the bootTraits
method in \Illuminate\Database\Eloquent\Model
.
With our trait now bootable we can get rid of the generateSlug
call from our model’s boot
method. It will just get applied to each model we attach the trait to.
Making the Trait Configurable
So far we’ve assumed that our slug will always be generated from the model’s title
property, but what if we need to use something else. For example, we might also have a Category
model that we want to attach the trait to that has a name
instead of a title
. We need to add a way of configuring our trait.
One way we can do this is by defining a method in each class exhibiting the trait that will return an array of settings. To ensure that this method exists we can define an abstract
method in our trait. So let’s add a requirement to our trait which we’ll just name after it (you can call it anything you like).
abstract public function sluggable(): array;
With this abstract method now defined any class that uses the trait will need to have this method defined otherwise an error will be thrown. So we add it to our Article
model and return an array of settings (in our abstract definition we’ve also required that the method returns an array).
public function sluggable() {
return [
'source' => 'title',
];
}
We can now update the boot method of our trait to use the settings.
public static function bootSluggable()
{
static::saving(function ($model) {
$settings = $model->sluggable();
$model->slug = $model->generateSlug($settings['source']);
});
}
Sluggable Trait
To wrap things up, here’s what the full trait looks like now.
<?php trait Sluggable { public static function bootSluggable() { static::saving(function ($model) { $settings = $model->sluggable(); $model->slug = $model->generateSlug($settings['source']); }); }
abstract public function sluggable(): array;
public function generateSlug($string) { return strtolower(preg_replace( ['/[^\w\s]+/', '/\s+/'], ['', '-'], $string )); } }
Then our Article
model with the booted trait looks like:-
<?php namespace App;
use Illuminate\Database\Eloquent\Model; use App\Traits\Sluggable;
class Article extends Model { use Sluggable; // Attach the Sluggable trait to the model
public function sluggable() { return [ 'source' => 'title', ]; } }
Finally, just to show the trait being applied to a second model we can consider the Category
model suggested earlier.
<?php namespace App;
use Illuminate\Database\Eloquent\Model; use App\Traits\Sluggable;
class Category extends Model { use Sluggable; // Attach the Sluggable trait to the model
public function sluggable() { return [ 'source' => 'name', ]; } }
So there you have it, bootable traits for Eloquent models with the ability to configure the trait’s settings.
Traits offer a useful means of writing DRY code in Laravel allowing us to share functionality across models and hooking into the Eloquent events without having to manually add code to each model’s boot
method each time we apply a trait. Just keep in mind that abstracting code like this can be a dangerous thing if you start to over complicate matters and as a result reduce the readability of your code. However, when used wisely can be a great way of sharing logic amongst your classes.