行为和行为事件

今天我们来谈谈“行为”和“行为事件”,当然,我们的重点是分析二者的实现机制。

行为

首先我们先回顾一下行为的几种绑定方式

覆盖yii\base\Component子类的behaviors方法,比如各位常见的ACF的配置
在yii\base\Component子类中,动态调用 yii\base\Component::attachBehaviors 方法附加行为,相比之下,第一种我们用的可能多一些
直接在配置文件中 components 同级配置,比如我们在集成 yii2-admin 时,就有一步是配置 “as access”
之前也碰到过很多同学问的一个有意思的问题:为啥要用行为,我自己写一个父类让子类继承不也是可以的吗?如果你也有这个问题,建议你回去把行为的那几篇实战文章再看看,动动手体会体会。

下面我们准备从行为的第三种绑定方式的实现开始谈一谈行为。

为此我们先按照第三种方式定义一个例子。

frontend/components目录下创建一个继承自 yii\base\Behavior 的类 A

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace frontend\components;

use Yii;
use yii\base\Behavior;

class A extends Behavior
{
public $aname = 'This is a';

public function callA()
{
var_dump(__METHOD__);
}
}

在配置文件 frontend/config/main.php 中添加如下配置

1
'as A' => frontend\components\A::className(),

该配置项注意和components同级,位置错了的自己记得改过来。

我们在 Site/index 操作下,可以这样调用 A::callA

1
Yii::$app->callA();

结果

1
string(28) "frontend\components\A::callA"

有同学虽然会用行为,但是用 yii\web\Application 的子类 Yii::$app 调用 callA 方法可能瞬间就懵了,其实很好理解,我们来分析一下。

预初始化阶段,我们说过,配置文件的配置是一个数组,假设用 $config 表示,$config 的 key 是yii\web\Application类的属性或者 setKey 方法,对吧?具体的实现方法是 yii\base\Component::__set(怎么到这一步的?不清楚的可以再看看 预初始化阶段的讲解),各位找到该方法,我就不贴代码了。

该方法中,第一个if会检测 setKey 方法是否存在,我们介绍过 yii\di\ServiceLocator::setComponents 方法,课后也让大家去了解 yii\base\Module::setModule 方法的实现,这都是第一个if的体现。

往下看,我们发现第二个if会判断$config的key,如果满足前三个字母是”on “(on后面有一个空格),则调用 yii\base\Component::on 方法给当前实例绑定事件,终于了解了配置事件的奥妙。

继续看,第三个if会判断$config的key的前三个字母是否是”as “(as后面有一个空格),如果满足,则会调用yii\base\Component::attachBehavior 为当前实例绑定行为。

即,我们现在又回到了文中开头说的第二种绑定行为的方式了,下面我们就第二第三种方式的实现来看一下 yii\base\Component::attachBehaviod 方法

1
2
3
4
5
public function attachBehavior($name, $behavior)
{
$this->ensureBehaviors();
return $this->attachBehaviorInternal($name, $behavior);
}

yii\base\Component::attachBehavior 为component(component指的是yii\base\Component及其子类的实例,阅读的过程中要分清)绑定行为,它的实现分为两部分

1、调用 yii\base\Component::ensureBehaviors 方法,把在 component::behaviors方法中声明的行为,绑定到component,这又回到了我们介绍的第一种绑定行为的方式上了,也就是说,虽然我们可以静态或者动态绑定以及配置行为,但,终归都是 yii\base\Component::ensureBehaviors和 yii\base\Component::attachBehaviorInternal 这两个方法的实现,这让我想起了当年高考题目老师常说的一句话,“万变不离其宗”。

好了,我们继续,yii\base\Component::ensureBehaviors 方法中,我们看到它的实现主要是循环 component::behaviors 方法,调用 yii\base\Component::attachBehaviorInternal 方法处理 component::behaviors 单元项,即为component绑定的行为。

yii\base\Component::attachBehaviorInternal 方法目的是为component绑定行为,我们截取该函数的部分代码看一下

1
2
3
4
5
6
7
8
9
10
if (is_int($name)) {
$behavior->attach($this);
$this->_behaviors[] = $behavior;
} else {
if (isset($this->_behaviors[$name])) {
$this->_behaviors[$name]->detach();
}
$behavior->attach($this);
$this->_behaviors[$name] = $behavior;
}

从上面的实现我们可以看到,不论是匿名函数还是非匿名函数,绑定行为可以概括为以下两点

$behavior->attach($this),调用 yii\base\Behavior::attach 方法关联component
$this->_behaviors[] = $behavior 或者 $this->_behaviors[$name] = $behavior,把行为添加到yii\base\Component::_behaviors 属性
yii\base\Behavior::attach 同样有两点作用(手动找到该方法),我们还以frontend\components\A 这个behavior为例

$this->owner = $owner,把 component 绑定到 frontend\components\A::owner属性,即 yii\base\Behavior::owner 属性
一旦行为类 frontend\components\A::events 方法有配置的事件,调用 $owner 即 Yii::$app 即 yii\base\Component::on 方法绑定事件,这是行为事件的一种实现,我们等下再细说
行为的三种绑定的实现我们说完了,下面我们继续来看看 Yii::$app->callA 方法的调用是如何实现的?

首先,yii\base\Application确实没有callA方法的实现,callA方法我们只在 frontend\component\A 类中有定义。

所以,当我们用 Yii::$app 调用callA方法时,yii\base\Component::__call 方法会被调起。

yii\base\Component::__call 的部分代码如下

1
2
3
4
5
foreach ($this->_behaviors as $object) {
if ($object->hasMethod($name)) {
return call_user_func_array([$object, $name], $params);
}
}

__call 方法在确定 Yii::$app 实例的 behaviors 方法配置的行为被绑定之后,循环 yii\base\Component::_behaviors 属性,该属性保存着component定义的所有行为。

循环component绑定的行为,并通过行为类的hasMethod判断被调用的方法callA是否存在,如果存在,则通过 call_user_func_array 方法调用。

其中这里的hasMethod方法指的是行为类 frontend\component\A 的父父类yii\base\Object::hasMethod方法,该方法会通过 method_exists($this, $name) 来判断 frontend\components\A::callA 方法是否存在。

综上所述,你明白为什么可以在component实例中自由的调用行为类的方法了吗?

还没完哈,回过头来我们再来看另外一个问题就很好理解了:打印下 Yii::$app->aname,看看得到什么结果?没错,我们同样可以得到frontend\components\A::aname属性。

其实现方法,自然要去找 yii\base\Component::__get 方法啦。

yii\base\Component::get 方法会先判断 component::getaname 是否存在,不存在,再去找行为的属性,代码体现在 yii\base\Component::get内

1
2
3
4
5
6
7
8
// behavior property
$this->ensureBehaviors();

foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) {
return $behavior->$name;
}
}

我们看一下 yii\base\Behavior::canGetProperty 方法的实现,即 yii\base\Object::canGetProperty

1
2
3
4
public function canGetProperty($name, $checkVars = true)
{
return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name);
}

很显然,frontend\components\A::aname 属性存在且可以访问。

行为事件

下面,让我们再来看看行为事件。

什么是行为事件?其实还是为了让 component 更强大。

其操作是在 behavior 的 events 方法中配置行为事件,比如我们应该都使用过的 yii\behaviors\TimestampBehavior,这就是一个行为事件的典型案例。

我们知道,但凡是把 yii\behaviors\TimestampBehavior 配置到对应的AR类中,AR关联的数据表的时间字段就会被赋值,操作很轻松,只是需要各位简单的手动配置下 TimestampBehavior 即可。

我们以 yii\behaviors\TimestampBehavior 来分析下行为事件的实现机制。

首先我们在AR类中配置TimestampBehavior行为

1
2
3
4
5
6
7
8
9
10
11
public function behaviors()
{
return [
[
'class' => TimestampBehavior::className(),
'createdAtAttribute' => 'created_at',
'updatedAtAttribute' => 'updated_at',
'value' => date('Y-m-d H:i:s'),
],
];
}

这是前提,行为都没有哪来的行为事件呢,对吧?

上面我们说道,在 yii\base\Component::ensureBehaviors 方法中,AR类的behaviors方法配置的行为会被绑定到component,component这里指的是AR实例,最终会调用 yiii\base\Behavior::attach方法绑定,该方法有一步循环 TimestampBehavior::events() 的操作。

我们注意到,TimestampBehavior::events方法指的是其父类yii\behaviors\AttributeBehavior::events,注意结合 TimestampBehavior::init 和 yii\base\Behavior::attach 方法分析,我们列一下部分代码可能更清楚一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// yii\behaviors\TimestampBehavior::init
if (empty($this->attributes)) {
$this->attributes = [
BaseActiveRecord::EVENT_BEFORE_INSERT => [$this->createdAtAttribute, $this->updatedAtAttribute],
BaseActiveRecord::EVENT_BEFORE_UPDATE => $this->updatedAtAttribute,
];
}

// yii\behaviors\AttributeBehavior::events
return array_fill_keys(
array_keys($this->attributes),
'evaluateAttributes'
);

// yii\base\Behavior::attach
foreach ($this->events() as $event => $handler) {
$owner->on($event, is_string($handler) ? [$this, $handler] : $handler);
}

所以最终你会发现,这里分别为 yii\base\BaseActiveRecord::EVENT_BEFORE_INSERT和yii\base\BaseActiveRecord::EVENT_BEFORE_UPDATE绑定的事件处理函数是 yii\behaviors\AttributeBehavior::evaluateAttributes方法

即,当AR调用save方法保存数据时,实际上我们要说的是触发事件 yii\base\BaseActiveRecord::EVENT_BEFORE_INSERT 和 yii\base\BaseActiveRecord::EVENT_BEFORE_UPDATE 时,这才在 yii\behaviors\AttributeBehavior::evaluateAttributes回调函数内,通过调用 $this->owner->$attribute 为属性赋值,到此,才算完成一套完整的行为事件的绑定和触发。

我们看到,虽然 TimestampBehavior 的使用非常简单,但是背后的逻辑并不简单,最后,我们再整理一下前后的执行顺序。

1
2
3
4
5
6
7
8
9
yii\behaviors\TimestampBehavior::init =>

yii\base\Behavior::attach =>

yii\behaviors\AttributeBehavior::events =>

yii\db\BaseActiveRecord::beforeSave =>

yii\behaviors\AttributeBehavior::evaluateAttributes