laravel 历险记之模型通过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 来添加监听器,也只是分析了添加监听器的原理。
有时间我会继续分享一下监听器的工作原理,其实也就是事件的工作原理。

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

Snail's Blog
请先登录后发表评论
  • 最新评论
  • 总共0条评论
  • 本博客使用免费开源的 laravel-bjyblog v5.5.1.3 -develop 搭建 © 2014-2018 www.snail-c.cn 版权所有 ICP证:蜀ICP备18023253号-1
  • 联系邮箱:459921737@qq.com