Laravel Eloquent 模型中通过 Trait 来添加监听器。

laravel Eloquent 模型中通过 Trait 来添加监听器。

Eloquent 模型会触发许多事件,让你在模型的生命周期的多个时间点进行监控:
retrieved, creating, created, updating,updated,
saving, saved,deleting, deleted, restoring, restored。
事件让你在每当有特定模型类进行数据库保存或更新时,执行代码。

上面是 laravel china 上 laravel 手册中对于 laravel Eloquent 的事件描述。
并且上面提供了两种在 Eloquent 模型中添加事件的方法:

  • 1.在 Eloquent 模型中添加 $dispatchesEvents 属性。
  /**
     * 此模型的事件映射.
     *
     * @var array
     */
    protected $dispatchesEvents = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
  • 2.使用观察者 Observer,并将观察者在服务提供者中的 boot 方法中注册。

这两种方法都很不错,但是在有些场景下应该会有更好的方法来实现 Eloquent 事件。

现在我举一个大家开发中应该很常见的一个场景:
添加操作者信息,
有人会说添加操作者还不简单啊,在每个对数据操作的地方将操作者信息数据一并写入就好了。
确认这样是可以,但是我觉得这样写不简洁优雅。
既然我们在用一个很优雅的框架,那么为什么我们不思考一下用更简洁的方式来实现呢。

下面我就用自己认为较为简洁的方式简单分享出来:

  • 3.通过给模型添加 Trait 来实现模型事件。

  • a.在你认为合适的地方新建一个用来添加操作人的 Trait 文件,内容如下。

<?php
/**
 * Created by PhpStorm.
 * User: coderChen
 * Date: 2018/9/2
 * Time: 16:51
 */

namespace App\Models\Traits;

use App\Observers\OperatorObserve;

trait OperatorTrait
{

    //这里模型实例化的时候,会执行该静态方法。
    public static function bootOperatorTrait(){
        //给模型绑定操作人监听器
        static::observe(OperatorObserve::class);
    }
}
  • b.在合适的地方,我们新建一个操作人监听器类 OperatorObserve,内容如下:
<?php

namespace App\Observers;

// creating, created, updating, updated, saving,
// saved,  deleting, deleted, restoring, restored
use Illuminate\Database\Eloquent\Model;

class OperatorObserver
{

    //新增修改会触发该方法
    public function saving(Model $model){
        //这里面做写入操作人处理逻辑
    }

    //删除会出发该方法
    public function deleting(Model $model){
        //这里面做写操作人处理逻辑
    }
}
  • c.最后一步就是在需要记录操作人的模型去 USE 这个 OperatorTrait 文件。
<?php

namespace App;

use App\Models\Traits\OperatorTrait;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    //通过 use OperatorTrait 来实现操作人自动写入。
    use OperatorTrait;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password','operator'
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];
}

到这里,通过给模型添加 Trait 来实现模型事件就完成了,可能有些同学会有一写疑问,为什么模型会自动执行 trait 中的某些方法,答案就在下面,
注意,这里你应该了解什么是静态延迟绑定,不然会被多个 static 调用给绕晕:

我们先看 Illuminate\Database\Eloquent 命名空间下的 Model 抽象类,
该类是所有 Eloquent 模型的基类,我们看到它的构造函数:

    /**
     * Create a new Eloquent model instance.
     *
     * @param  array  $attributes
     * @return void
     */
    public function __construct(array $attributes = [])
    {

        $this->bootIfNotBooted();

        $this->syncOriginal();

        $this->fill($attributes);
    }

接下来我们去看到构造函数中调用的 bootIfNotBooted() 方法:


    /**
     * Check if the model needs to be booted and if so, do it.
     *
     * @return void
     */
    protected function bootIfNotBooted()
    {
        //如果该模型未被实例化过,就去执行 if 里面的代码,去实例化它。
        if (! isset(static::$booted[static::class])) {
            //将模型标注为已实例化。
            static::$booted[static::class] = true;
            $this->fireModelEvent('booting', false);
            //通过静态延迟绑定去执行该模型的 boot() 启动方法
            static::boot();

            $this->fireModelEvent('booted', false);
        }
    }

接下来看到在 bootIfNotBooted() 中的 boot() 方法,另外两个 fireModelEvent() 暂时先不管:

/**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        static::bootTraits();
    }

接下来去看到 boot() 方法中的 bootTraits() 方法,欸,等下,我们关注一下这个方法的名字,启动 traits,在模型中能自动运行 trait 中的某些方法应该就是它的功劳了:

/**
     * Boot all of the bootable traits on the model.
     *
     * @return void
     */
    protected static function bootTraits()
    {
        //获取该模型的类名,包含其命名空间,比如这样:App\User
        $class = static::class;

        //用于存放已经执行过的方法缓存
        $booted = [];

        //class_user_recursive 方法获取该类以及该类的父类所有的 trait
        foreach (class_uses_recursive($class) as $trait) {

            //通过给 trait 名称前面添加 boot 来生成对应 trait 中的 启动方法
            $method = 'boot'.class_basename($trait);

            //检查模型类中是否存在 trait 中的 自动启动方法,并且检查该方法是否已经被执行过。
            if (method_exists($class, $method) && ! in_array($method, $booted)) {

                //执行 trait 中的方法
                forward_static_call([$class, $method]);

                //将已经执行过的方法放入放到 已执行方法的缓存中,防止重复执行
                $booted[] = $method;
            }
        }
    }

好了,到这儿就算是已经知道了 Eloquent 模型中的启动方法执行的原理了,
在 githab 上有一个 laravel Eloquent 的数据验证包也是运用的上面的原理,链接在此,有兴趣的同学可以去研究下。

其实这里还要注意的一点就是:

对于 trait 中的启动方法命名应该是在 trait 名称前面添加 boot 字符。

当然,这里只是说了如何通过 trait 来添加监听器,也只是分析了添加监听器的原理。
往后有时间我会继续分享一下监听器的工作原理,其实也就是事件的工作原理。

当然以上都是作者自己的看法,如果大家认为我的看法可以更进一步改进的话,欢迎留言讨论。

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 3年前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 6
Jourdon

更简单的方法

public static function bootSlugTrait()
    {
    //creating, created, updating, updated, saving,
// saved,  deleting, deleted, restoring, restored
//
        static::saving(function ($model) {
            return $model->method();//这个方法随便起的,对应下面的方法
        });
    }

protected function method() 
    {
        //to do something
    }

相应Model 里直接use 这个trait就可以了,

这里只是提个简单方法,但不建议这么用。

5年前 评论

@Jourdon 是的,也可以在 trait 中启动方法中调用
static::saving
static::deleteing
等方法来注册对应的观察闭包。

5年前 评论

更方便代码低耦合了

5年前 评论

@Jourdon 请恕在下愚昧,这样写好像不能注入其他实例,比较有局限性. 请大佬指点12

5年前 评论
Jourdon

@飘儿白 你说的对,正常写法还是要用楼主的方法,独立出来一个是扩展复用,另一个是方便理解,我写的方法只是简单,但不一定实用,不过在开发composer包倒是比较方便。

5年前 评论
panda-sir

干脆点:

    protected static function boot()
    {
        parent::boot();
        static::observe(OperatorObserve::class);
    }

:laughing:

5年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!