模型事件在 Laravel 的世界中,你对 Eloquent 大多数操作都会或多或少的触发一些模型事件,下面这篇文章主要给大家介绍了关于Laravel模型事件的实现原理,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴。

前言

Laravel的ORM模型在一些特定的情况下,会触发一系列的事件,目前支持的事件有这些:creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored,那么在底层是如何实现这个功能的呢?

下面话不多说了,来一起看看详细的介绍吧。

1.如何使用模型事件

先来看看如何使用模型事件,文档里面写了两种方法,实际上总共有三种方式可以定义一个模型事件,这里以saved事件来做例子,其他事件都一样。

1.events属性

直接上代码:

1
2
3
4
5
6
7
class User extends Authenticatable {
 use Notifiable;
 
 protected $events = [
  'saved' => UserSaved::class,
 ];
}

这个比较难以理解,而且文档并没有详细说明,刚开始以为saved被触发后会调用UserSaved里面的handle方法,实际上并不是。这个数组只是对事件做的一个映射,它定义了在模型的saved的时候会触发UserSaved这个事件,我们还要定义该事件以及其监听器才可以:

1
2
3
4
5
6
7
8
namespace App\Events;
use App\User;
class UserSaved {
 public $user;
 public function __construct(User $user){
  $this->user = $user;
 }
}
1
2
3
4
5
6
namespace App\Listeners;
class UserSavedListener {
 public function handle(UserSaved $userSaved){
  dd($userSaved);
 }
}

然后还要到EventServiceProvider中去注册该事件和监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class EventServiceProvider extends ServiceProvider
{
 /**
  * The event listener mappings for the application.
  *
  * @var array
  */
 protected $listen = [
  'App\Events\UserSaved' => [
   'App\Listeners\UserSavedListener',
  ]
 ];
 
 /**
  * Register any events for your application.
  *
  * @return void
  */
 public function boot()
 {
  parent::boot();
 }
}

这样在saved节点的时候,UserSaved事件会被触发,其监听器UserSavedListener的handle方法会被调用。

2.观察者

这是文档比较推崇的一个模型事件定义方法,也比较好理解,先定义一个观察者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use App\User;
class UserObserver
{
 /**
  * 监听用户创建事件.
  *
  * @param User $user
  * @return void
  */
 public function created(User $user)
 {
  //
 }
 
 /**
  * 监听用户创建/更新事件.
  *
  * @param User $user
  * @return void
  */
 public function saved(User $user)
 {
  //
 }
}

然后在某个服务提供者的boot方法中注册观察者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace App\Providers;
use App\User;
use App\Observers\UserObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
 /**
  * Bootstrap any application services.
  *
  * @return void
  */
 public function boot()
 {
  User::observe(UserObserver::class);
 }
 
 /**
  * Register the service provider.
  *
  * @return void
  */
 public function register()
 {
  //
 }
}

这样在模型事件触发的时候,UserObserver的相应方法就会被调用。其实,在使用观察者的时候,除了一些系统自带的,我们还可以定义一些自己的事件:

1
2
3
4
5
6
class User extends Authenticatable {
 use Notifiable;
 protected $observables = [
  'customing', 'customed'
 ];
}

然后在观察者里面定义同名方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class UserObserver
{
 /**
  * 监听用户创建/更新事件.
  *
  * @param User $user
  * @return void
  */
 public function saved(User $user)
 {
  //
 }
 
 public function customing(User $user){
 }
 
  public function customed(User $user){
 }
}

由于是我们自己定义的事件,所以触发的时候也必须手动触发,在需要触发的地方调用模型里面的一个fireModelEvent方法即可。不过由于该方法是protected的,所以只能在自己定义的模型方法里面,当然如果通过反射来调用,或许可以直接在$user对象上触发也说不定,这个我没试,大家可以自行测试下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class User extends Authenticatable {
 use Notifiable;
 protected $observables = [
  'customing', 'awesoming'
 ];
  
 public function custom(){
  if ($this->fireModelEvent('customing') === false) {
   return false;
  }
   
  //TODO
   if ($this->fireModelEvent('customed') === false) {
   return false;
  }
 }
}

3.静态方法定义

我们还可以通过模型上的对应静态方法来定义一个事件,在EventServiceProvider的boot方法里面定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class EventServiceProvider extends ServiceProvider{
 /**
  * Register any events for your application.
  *
  * @return void
  */
 public function boot()
 {
  parent::boot();
  User::saved(function(User$user) {
  });
  User::saved('UserSavedListener@saved');
 }
}

通过静态方法定义的时候,可以直接传递进入一个闭包,也可以定义为某个类的方法,事件触发时候传递进入的参数就是该模型实例。

2.模型事件实现原理

Laravel的模型事件所有的代码都在Illuminate\Database\Eloquent\Concerns\HasEvents这个trait下,先来看看Laravel是如何注册这些事件的,其中的$dispatcher是一个事件的调度器Illuminate\Contracts\Events\Dispatcher实例,在Illuminate\Database\DatabaseServiceProvider的boot方法中注入。

1
2
3
4
5
6
protected static function registerModelEvent($event, $callback){
  if (isset(static::$dispatcher)) {
   $name = static::class;
   static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback);
  }
 }

这里是Laravel事件注册的地方,其中以eloquent.saved:App\User为事件名,$callback作为处理器来注册。这个注册事件的方法,只会注册以观察者和静态方法定义的。假如你定义为模型的$events属性的话,Laravel是不会注册的,会在触发事件的时候同步触发,接下来会分析。

然后在HasEvents中定义了一堆的方法如下,这些就是我们上面通过静态方法来定义事件监听器的原理,不多说一看就懂。

1
2
3
4
5
6
7
public static function saving($callback){
 static::registerModelEvent('saving', $callback);
}
 
public static function saved($callback){
 static::registerModelEvent('saved', $callback);
}

那么如何通过观察者的形式来定义事件监听器呢?看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static function observe($class){
 $instance = new static;
 $className = is_string($class) ? $class : get_class($class);
 
 foreach ($instance->getObservableEvents() as $event) {
  if (method_exists($class, $event)) {
   static::registerModelEvent($event, $className.'@'.$event);
  }
 }
}
 
public function getObservableEvents()
{
 return array_merge(
  [
   'creating', 'created', 'updating', 'updated',
   'deleting', 'deleted', 'saving', 'saved',
   'restoring', 'restored',
  ],
  $this->observables
 );
}

先获取到observer的类名,然后判断是否存在事件名对应的方法,存在则调用registerModelEvent注册,这里事件名还包括我们自己定义在observables数组中的。

事件以及监听器都定义好后,就是如何触发了,前面说到有一个方法fireModelEvent,来看看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected function fireModelEvent($event, $halt = true)
{
 if (! isset(static::$dispatcher)) {
  return true;
 }
 
 $method = $halt ? 'until' : 'fire';
 
 $result = $this->filterModelEventResults(
  $this->fireCustomModelEvent($event, $method)
 );
 
 if ($result === false) {
  return false;
 }
 
 return ! empty($result) ? $result : static::$dispatcher->{$method}(
  "eloquent.{$event}: ".static::class, $this
 );
}

其中比较关键的一个方法是fireCustomModelEvent,它接受一个事件名以及触发方式。顺带一提,filterModelEventResults这个方法的作用就是把监听器的返回值为null的过滤掉。

看看fireCustomModelEvent的源码:

1
2
3
4
5
6
7
8
9
10
11
protected function fireCustomModelEvent($event, $method)
{
 if (! isset($this->events[$event])) {
  return;
 }
 
 $result = static::$dispatcher->$method(new $this->events[$event]($this));
 if (! is_null($result)) {
  return $result;
 }
}

这个就是用来触发我们通过$events定义的事件了,假如我们这么定义:

1
2
3
4
5
class User extends Model{
 protected $events = [
  'saved' => UserSaved::class
 ]
}

那这里的触发就是:

1
$result = static::$dispatcher->fire(new UserSaved($this));

顺带一提,Laravel中触发事件的方法有两个,一个是常用的fire,还有一个是util,这两个的差别就是fire会把监听器的返回值返回,而util永远返回null

然后接下来就是会去触发通过观察者和静态方法定义的监听器了,这一段代码:

1
2
3
4
5
6
7
if ($result === false) {
 return false;
}
 
return ! empty($result) ? $result : static::$dispatcher->{$method}(
 "eloquent.{$event}: ".static::class, $this
);

这里会先判断$events定义的监听器是否返回false以及返回值是否为空,如果为false则直接结束事件,如果返回不为false而且为空的话,会再去触发通过观察者和静态方法定义的监听器,并且把监听器的返回值返回。

原文:https://www.jb51.net/article/136377.htm

Laravel模型事件的实现原理详解的更多相关文章

  1. epoll原理详解及epoll反应堆模型

    本文转载自epoll原理详解及epoll反应堆模型 导语 设想一个场景:有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收TCP包),也就是说在每一时刻 ...

  2. Java网络编程和NIO详解6:Linux epoll实现原理详解

    Java网络编程和NIO详解6:Linux epoll实现原理详解 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Java网络编程和NIO h ...

  3. select用法&原理详解(源码剖析)(转)

    今天遇到了在select()前后fd_set的变化问题,查了好久终于找到一个有用的帖子了,很赞,很详细!!原文链接如下: select用法&原理详解(源码剖析) 我的问题是: 如下图示:在se ...

  4. 块级格式化上下文(block formatting context)、浮动和绝对定位的工作原理详解

    CSS的可视化格式模型中具有一个非常重要地位的概念——定位方案.定位方案用以控制元素的布局,在CSS2.1中,有三种定位方案——普通流.浮动和绝对定位: 普通流:元素按照先后位置自上而下布局,inli ...

  5. [转]js中几种实用的跨域方法原理详解

    转自:js中几种实用的跨域方法原理详解 - 无双 - 博客园 // // 这里说的js跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同 ...

  6. 【转】VLAN原理详解

    1.为什么需要VLAN 1.1 什么是VLAN? VLAN(Virtual LAN),翻译成中文是“虚拟局域网”.LAN可以是由少数几台家用计算机构成的网络,也可以是数以百计的计算机构成的企业网络.V ...

  7. 【转载】JAVA消息服务JMS规范及原理详解

    转载:https://www.cnblogs.com/molao-doing/articles/6557305.html 作者: moyun- 一.简介 JMS即Java消息服务(Java Messa ...

  8. JAVA消息服务JMS规范及原理详解

    JAVA消息服务JMS规范及原理详解 一.简介 JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应 ...

  9. [No0000126]SSL/TLS原理详解与WCF中的WS-Security

    SSL/TLS作为一种互联网安全加密技术 1. SSL/TLS概览 1.1 整体结构 SSL是一个介于HTTP协议与TCP之间的一个可选层,其位置大致如下: SSL:(Secure Socket La ...

随机推荐

  1. Java的基本知识之线程池篇

    1.基本概念 1.共享资源 多个线程对同一份资源进行访问(读写操作),该资源被称为共享资源.如何保证多个线程访问到的数据是一致的,则被称为数据同步或资源同步. 2.线程通信 线程通信,又叫进程内通信, ...

  2. 如何选择优秀的APS系统供应商?问自己这几个问题!

    高级的计划和排程和生产排程软件的好处是巨大的.然而,生产排程软件不是商品,尚不能保证您选择的任何高级的计划和排程软件能满足您的所有需求. 那么,我们来谈谈如何选择适合你的公司最好的生产排程软件.这需要 ...

  3. android之自定义viewGroup仿scrollView的两种实现(滚动跟粘性)

    最近一直在研究自定义控件,一般大致分为三种情况:自绘控件,组合控件,继承控件.接下来我们来看下继承控件.在此借鉴一位博主的文章,补充粘性的实现效果,并且加注自己的一些理解.大家也可以查看原文博客.an ...

  4. linux下测试某网址或IP端口能否访问

    参考这篇 Linux系统下怎么测试端口的连通性 测试端口连通 nc -v -w 1 183.xx.oo.ee -z 82 1linux主机centos操作系统,项目里面需要访问一个URL路径的接口ht ...

  5. vue中使用echart柱状图

    一: <template> <Layout> <Content> <Card :style="{minHeight:'300px'}"&g ...

  6. day 01 预科

    目录 作业 二,Markdown基本语法 一级标题 二级标题 三级标题 作业 二,Markdown基本语法 标题 一级标题 二级标题 三级标题 四级标题 加粗 哦,更粗了 斜体 咦,我歪了 高亮 == ...

  7. Gtest:源码解析

    转自:玩转Google开源C++单元测试框架Google Test系列(gtest)之七 - 深入解析gtest 一.前言 “深入解析”对我来说的确有些难度,所以我尽量将我学习到和观察到的gtest内 ...

  8. struct并不报错

    struct { int item; struct list* next; }list; 如果结构体定义如上,使用下面的代码,将会报错 //添加元素,由于我们实现的是单向链表,所以使用从尾部添加 bo ...

  9. 一个线上JVM的CPU资源占用过高问题的排查

    原文:https://www.iteye.com/blog/tyrion-2293369 上午线上某应用的一台JVM的CPU占比突然飙高到192%,并且一直下不来,导致监控一直告警,好久没处理这种问题 ...

  10. Cisco网络模拟器踩坑记录

    1.在我们新建一个拓扑图的时候,选择设备之间的连线种类有时会导致线路不通的情况(两个端口上为红色点)这时候建议拆除这条线后选择闪电标记 的万能线帮助我们自动创建连线(这时就能根据它显示的线条种类得知应 ...