Using Laravel's Bootable Eloquent Traits

Using Laravel's Bootable Eloquent Traits

The original link

 

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.

上一篇:20 个 Laravel Eloquent 必备的实用技巧


下一篇:Laravel Eloquent ORM之入门