使用行为(behavior)可以在不修改现有类的情况下,对类的功能进行扩充。通过将行为绑定到一个类,可以使得类具有行为本身所具有的属性和方法,就好像是类本来就具有的这些属性和功能一样。
好的代码设计,必须要同时满足可复用性、可维护性和可扩展性。设计原则中有一条非常重要的一条:类应该对扩展开放,对修改关闭(开闭原则)。改变原有代码往往会带来潜在风险,因此我们尽量减少修改的行为。我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可以搭配新的行为。如果能实现这样的目标,有什么好处呢?这样的设计具有弹性,可以应对改变,可以接收新的功能来应对改变的需求。
Yii的行为就是这样一类对象,当一个对象(继承了Component的)想要扩展功能,又不想改变原有代码时,那么你完全可以用行为去实现这些新功能,然后绑定到该对象上——完全是符合“开闭原则”的。 Yii的行为都需要继承自yiiaseBehavior
,而能接受行为绑定从而扩充自身功能的只能是yiiaseComponent的子类,只继承BaseObject基类没有继承Component的不能享受此“待遇”。因此,行为是组件才有的功能。行为和事件结合起来使用,还可以定义组件在何种事件进行何种反馈。
行为是 yiiaseBehavior 或其子类的实例。 行为,也称为 mixins, 可以无须改变类继承关系即可增强一个已有的 组件 类功能。 当行为附加到组件后,它将“注入”它的方法和属性到组件, 然后可以像访问组件内定义的方法和属性一样访问它们。 此外,行为通过组件能响应被触发的事件,从而自定义或调整组件正常执行的代码。
因此行为有如下两个作用:
- 将属性和方法注入到一个component里面,被访问时和别的属性或者方法访问无异(行为的附加)
- 响应component中触发的事件,以便对某一事件作出反应(行为的绑定(事件)和触发,是对事件的应用)
行为的附加
行为的附加或者绑定,通常是由Component来发起。有两种方式可以将一个Behavior绑定到一个 yiiaseComponent 。 一种是静态附加行为,另一种是动态附加行为。
静态附加在实践中用得比较多一些,因为一般情况下,在你的代码没跑起来之前,一个类应当具有何种行为是确定的。 动态附加主要是提供了更灵活的方式,上面即是行为的动态附加,但实际使用中并不多见。
1. 静态附加
通过重写 behaviors 方法。
class User extends ActiveRecord { const MY_EVENT = 'my_event'; public function behaviors() { return [ // 匿名行为,只有行为类名 MyBehavior::className(), // 命名行为,只有行为类名 'myBehavior2' => MyBehavior::className(), // 匿名行为,配置数组 [ 'class' => MyBehavior::className(), 'prop1' => 'value1', 'prop2' => 'value2', ], // 命名行为,配置数组 'myBehavior4' => [ 'class' => MyBehavior::className(), 'prop1' => 'value1', 'prop2' => 'value2', ] ]; } }
上面的数组响应的键就是行为的名称,这种行为成为命名行为,没有指定名称的就成为匿名行为。
还有一个静态的绑定办法,就是通过配置文件来绑定:
as 前缀方法.
[ 'class' => User::className(), 'as myBehavior2' => MyBehavior::className(), 'as myBehavior3' => [ 'class' => MyBehavior::className(), 'prop1' => 'value1', 'prop3' => 'value3', ], ]
2. 动态附加
要动态附加行为,在对应组件里调用 yiiaseComponent::attachBehavior()
方法即可,如:
use appcomponentsMyBehavior; // 附加行为——对象 $user->attachBehavior('myBehavior1', new MyBehavior); // 附加行为——类名 $user->attachBehavior('myBehavior2', MyBehavior::className()); // 附加行为——配置数组 $user->attachBehavior('myBehavior3', [ 'class' => MyBehavior::className(), 'prop1' => 'value1', 'prop2' => 'value2', ]);
也可以通过 yiiaseComponent::attachBehaviors()
同时附加多个行为:
$myBehavior = new MyBehavior(); $user->attachBehaviors([ 'myBehavior1'=> $myBehavior, [ 'class' => MyBehavior2::className(), 'prop1' => 'value1', 'prop3' => 'value2', ], new MyBehavior3() ]);
附加多个行为,那么组件就获得了所有这些行为的属性和方法。
不管是静态附加还是动态附加,命名行为都可以通过yiiaseComponent::getBehavior($name)获取出来,匿名行为不可以单独获取出来,但是可以通过Component::getBehaviors()
一次全部获取出来。
行为附加的原理
下面我们看下行为的属性,是如何注入到类中,然后被类调用的。
在Component内部,事件是通过私有属性$_event
保存事件及其处理器,和事件类似,行为是通过私有属性$_behavior
来保存的:
private $_events = []; private $_behaviors;
$_behaviors的数据结构:
上图中前面两个是命名行为,后面两个是匿名行为。数组的每个元素值都是Behavior的子类实例。
行为附加涉及到四个方法:
Component::behaviors() Component::ensureBehaviors() Component::attachBehaviorInternal() Behavior::attach()
Component::behaviors()用于供子类覆写(方法重写),比如:
public function behaviors() { return [ 'timeStamp' => [ 'class' => TimeBehavior::className(), 'create' => 'create_at', 'update' => 'update_at', ], ]; }
yiiaseComponent::ensureBehaviors()方法经常出现,它的作用是将各种动态的和静态的方式附加的行为变成标准格式(参看$_behaviors的数据结构):
public function ensureBehaviors() { if ($this->_behaviors === null) { $this->_behaviors = []; // behaviors()方法由Component的子类重写 foreach ($this->behaviors() as $name => $behavior) { $this->attachBehaviorInternal($name, $behavior); } } }
我们会发现 yiiaseComponent 类中基本所有的方法(和属性或者方法相关的方法,为了属性注入)里面都调用了 ensureBehaviors() 此方法,为什么要这样做呢?目的就是为了确保该方法一定会被执行到。
这就是为什么我们在系统中没有单独提议的去执行调用 他(如在 init()方法去调用),但是最后 ensureBehaviors()一样被执行了,因为在系统在进行组件,或者其他初始化时,一定会调用到 yiiaseComponent 类中的接触方法,
如 yiiaseComponent ::__set(), yiiaseComponent __get() ,等,因为这些在进行组件初始化动态赋值时,肯定会被调用到。
你想测试下系统什么时候最开始调用的 ensureBehaviors() 方法,你可以在该方法中输出trace 。。在方法中加入 print_r(debug_trace()) 就可以看到 方法被调用了
既然 ensureBehaviors 肯定会被执行,就是说 只要我们重写子类的 behaviors(),只要有行为,肯定就会被绑定到类中
接下来的第三个出场的attachBehaviorInternal(),我们看看是何方神圣:
private function attachBehaviorInternal($name, $behavior) { //如果是配置数组,那就将其创建出来再说 if (!($behavior instanceof Behavior)) { $behavior = Yii::createObject($behavior); } if (is_int($name)) { // 匿名行为 //先是行为本身和component绑定 $behavior->attach($this); //$this 的妙用,将自身和行为直接进行绑定,行为的owner 就是组件(Componet)对象 ,这样就将两者建立了关系,随处可调用。 //将行为放进$_behaviors数组,没有键值的是匿名行为 $this->_behaviors[] = $behavior; } else { //命名行为 if (isset($this->_behaviors[$name])) { //命名行为需要保证唯一性 $this->_behaviors[$name]->detach(); } $behavior->attach($this); //命名行为,键值就是行为名称 $this->_behaviors[$name] = $behavior; } return $behavior; }
Yii中以Internal开头或者结尾的,一般是私有方法,往往都是命门所在,如果要看源码,这些都是核心逻辑实现的地方。
最后一个出场的是Behavior::attach()
,Behavior有一个属性$owner
,指向是拥有它的组件,就是行为的拥有者。组件和行为是一个相互绑定、相互持有的过程。组件在$_behavior
持有行为的同时,行为也在$owner
中持有组件。
因此,不管是行为的附加还是解除都是双方的事情,不是一方能说了算的。
public function attach($owner) { //Behavior的$owner指向的是行为的所有者 $this->owner = $owner; //让行为的所有者$owner绑定用户在Behavior::events()中所定义的事件和处理器 foreach ($this->events() as $event => $handler) { $owner->on($event, is_string($handler) ? [$this, $handler] : $handler); } }
这个方法就将行为里面定义的 event 事件 全部注册到了 owner 组件上了。也就是说 组件 owner 不仅拥有了行为,且同时拥有了组件所定义的事件。
行为的解除
有附加当然有解除,命名行为可以被单个解除,使用方法Component::detachBehavior($name)
,匿名行为不可以单独解除,但是可使用detachBehaviors()
方法解除所有的行为。
//解除命名行为 $user->detachBehavior('myBehavior1'); //解除所有行为 $user->detachBehaviors();
这上面两种方法,都会调用到 yiiaseBehavior::detach() ,其代码如下:
public function detachBehavior($name) { $this->ensureBehaviors(); if (isset($this->_behaviors[$name])) { $behavior = $this->_behaviors[$name]; //1.将行为从$owner的$_behaviors中删除 unset($this->_behaviors[$name]); //2.解除$owner的所有事件和其处理器 $behavior->detach(); return $behavior; } return null; }
而$behavior->detach()
是这样的:
public function detach() { if ($this->owner) { //解绑$owner所有事件和其事件处理器 foreach ($this->events() as $event => $handler) { $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler); } //$owner重新置为null,表示没有任何拥有者 $this->owner = null; } }
在 Component 类中,我们看见 __clone 组件时,会将所有的 行为,事件都清空
public function __clone() { $this->_events = []; $this->_eventWildcards = []; $this->_behaviors = null; }
行为所要响应的事件
行为与事件结合后,可以在不对类作修改的情况下,补充类在事件触发后的各种不同反应。因此,只需要重载 yiiaseBehavior::events() 方法,表示这个行为将对类的何种事件进行何种反馈即可:
class MyBehavior extends Behavior { public $attr; public function events() //覆写events方法 { return [ ActiveRecord::EVENT_BEFORE_INSERT => 'beforeInsert', //将事件和事件处理器绑定 User::MY_EVENT => [$object, 'methodName'],//自己定义的事件 ]; } //$event可以带来三个信息,事件名,触发此事件的对象(类或者实例),附加的数据 public function beforeInsert($event) { $model = $this->owner;//访问已附件的组件 // Use $model->attr } public function methodName($event) { $owner = $this->owner;//行为的拥有者 $sender = $event->sender//触发此事件的类或者实例 $data = $event->data;//触发事件时传递的参数 // Use $model->attr } }
events()方法返回一个关联数组,键是事件名,值是要响应的事件处理器。事件处理器可以是一下四种形式:
- 此行为中的方法
methodName
,等效为[$this, 'methodName']
- 对象的方法:
[$object, 'methodName']
- 类的静态方法:
['Page', 'methodName']
- 闭包:
function ($event) { ... }
总结:行为被绑定到类中的过程。
1. 系统本身初始化,会调用类 Component::ensureBehaviors() 方法;
2. Component::ensureBehaviors() 方法 ,会调用 $this->behaviors(),也就是我们在子类中重写的方法;
3. 调用ensureBehaviors() 方法,就会将规范好的行为对象,绑定到到 Component组件上(子类),同时保存行为 到 $this->_behaviors 的私有变量中;接着将行为中定义的event()事件 注册到组件上,保存到 $this->_events 的私有变量中。
4. 当我们在行为中定义的是系统中已有的 事件, 如 Model 中的 beforeSave, afterSave 等事件,该事件自然就会被触发了。
5. Component 类中的 __set(), __get(), __call() 方法中 都会通过 $this->_behaviors 拿到行为对象,然后在行为对象中 找 变量,或者 方法 ,这样就达到了属性的注入了,所以就如我们说 Component子类 调用 行为的属性就像调用自己的属性一样。
行为响应事件的实例
Yii费了那么大劲,主要就是为了将行为中的事件handler绑定到类中去。因为在编程中用的最多的,也就是Component对各种事件的响应。通过行为注入,可以在不修改现有类的代码的情况下更改、扩展类对于事件的响应和支持。使用这个技巧,可以玩出很酷的花样出来。 比如,Yii自带的 yiiehaviorsAttributeBehavior
类,定义了在一个 ActiveRecord 对象的某些事件发生时, 自动对某些字段进行修改的行为。它有一个很常用的子类 yiiehaviorsTimeStampBehavior 用于将指定的字段设置为一个当前的时间戳。现在以它为例子说明行为的运用。 在 yiiehaviorsAttributeBehavior::event() 中,代码如下:
public function events() { return array_fill_keys( array_keys($this->attributes), 'evaluateAttributes' ); }
代码很容易看懂,无需详述。
而在yiiehaviorsTimeStampBehavior::init()中有代码:
public function init() { parent::init(); if (empty($this->attributes)) { //重点看这里 $this->attributes = [ BaseActiveRecord::EVENT_BEFORE_INSERT => [$this->createdAtAttribute, $this->updatedAtAttribute], BaseActiveRecord::EVENT_BEFORE_UPDATE => $this->updatedAtAttribute, ]; } }
上面的这个方法是初始化$this->attributes
这个数组。结合前面的两个方法,返回的$event数组应该是这样的:
return [ BaseActiveRecord::EVENT_BEFORE_INSERT => 'evaluateAttributes', BaseActiveRecord::EVENT_BEFORE_UPDATE => 'evaluateAttributes', ];
这里的意思是BaseActiveRecord::EVENT_BEFORE_INSERT
和BaseActiveRecord::EVENT_BEFORE_UPDATE
都响应处理器evaluateAttributes
。看看其关键部分:
public function evaluateAttributes($event) { ... if (!empty($this->attributes[$event->name])) { $attributes = (array) $this->attributes[$event->name]; //这里默认返回默认的时间戳time() $value = $this->getValue($event); foreach ($attributes as $attribute) { if (is_string($attribute)) { if ($this->preserveNonEmptyValues && !empty($this->owner->$attribute)) { continue; } //将其赋值给$owner的字段 $this->owner->$attribute = $value; } } } }
使用时,只需要在ActiveRecord里面重载behaviors()方法:
public function behaviors() { return [ [ 'class' => TimestampBehavior::className(), 'attributes' => [ ActiveRecord::EVENT_BEFORE_INSERT => 'created_at', ActiveRecord::EVENT_BEFORE_UPDATE => 'updated_at', ] ], ]; }
因此,当EVENT_BEFORE_INSERT
事件触发, 这样,你在插入记录时created_at
和updated_at
会自动更新,而在修改时updated_at
会更新。
行为的属性和方法注入原理
通过以上各个例子,组件附加了行为之后,就获得了行为的属性和方法。那么,这是如何实现的呢?归根结底主要通过__set(),__get(),__call()
这些魔术方法来实现的。属性的注入靠的是__set(),__get()
,而方法的注入是靠__call()
。
属性的注入
Component持有一个数组$_behavior,里面都是Behavior子类,而Behavior继承自Yii最基础的BaseObject。在《Yii2基本概念之——属性(property)》中我们介绍了属性的概念,因此Behavior也是可以运用属性的。
Component的可读属性,我们看看Component的getter函数:
public function __get($name) { $getter = 'get' . $name; //这是自己的可写属性 if (method_exists($this, $getter)) { return $this->$getter(); } /**下面是比BaseObject多出来的部分**/ $this->ensureBehaviors(); //依次检查各个行为中的可读属性 foreach ($this->_behaviors as $behavior) { if ($behavior->canGetProperty($name)) { return $behavior->$name; } } ... }
Component的可写属性,我们看看Component的setter函数:
public function __set($name, $value) { $setter = 'set' . $name; //自己的可写属性 if (method_exists($this, $setter)) { $this->$setter($value); return; } elseif (strncmp($name, 'on ', 3) === 0) { $this->on(trim(substr($name, 3)), $value); return; } elseif (strncmp($name, 'as ', 3) === 0) { $name = trim(substr($name, 3)); $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value)); return; } $this->ensureBehaviors(); //依次检查各个行为中是否有可写属性$name foreach ($this->_behaviors as $behavior) { if ($behavior->canSetProperty($name)) { $behavior->$name = $value; return; } } ... }
对于setter函数,略微复杂,检查顺序依次是:
- 自己的setter函数,也即是自己的可写属性
- 如果
$name
是'on xyz'形式,则xyz作为事件,$value作为handler,将其绑定 - 如果
$name
是'as xyz'形式,则xyz作为行为名字,$value作为行为,将其附加 - 依次检查各个行为中是否有可写属性$name,返回第一个;如果没有则抛出异常
因此,Component的可读属性就是本身的可读属性加上所有行为的可读属性;而可写属性就是本身的可写属性加上所有行为的可写属性。
方法的注入
同属性的注入类似,方法的注入也是自身的方法加上所有行为的方法:
public function __call($name, $params) { $this->ensureBehaviors(); //遍历所有行为的方法 foreach ($this->_behaviors as $object) { if ($object->hasMethod($name)) { return call_user_func_array([$object, $name], $params); } } ...
这里以为最终调用的是call_user_func_array()
的函数,所以只有行为的public 方法才能被注入组件中。 除了属性的读和写,还有对属性的判断(isset)和注销(unset),分别通过对魔术方法__isset
和__unset
的重载来实现,这里就不多赘述了。
结语
属性,事件和行为是Yii的基础功能,它们使得Yii成为一个变化无穷、魅力无穷的框架。然而,框架不能做PHP本身都做不到的事情,它酷炫的功能无非是PHP自身的面向对象特性(重载,魔术方法,成员变量/函数可见性)和一些数据结构,外加巧妙的算法来实现的。因此“解剖”的目的就在于,解开这次神秘面纱,搞清楚内在逻辑,最终使得自己的编程能力得到切实的提高。