yii2.0 行为
使用行为(behavior)可以在不修改现有类的情况下,对类的功能进行扩充。 通过将行为绑定到一个类,可以使类具有行为本身所定义的属性和方法,就好像类本来就有这些属性和方法一样。 而且不需要写一个新的类去继承或包含现有类。
yii中的行为,其实是yii\base\behavior类的实例, 只要将一个behavior实例绑定到任意的yii\base\component实例上, 这个component就可以拥有该behavior所定义的属性和方法了。而如果将行为与事件关联起来,可以玩的花样就更多了。
但有一点需要注意,behavior只能与component类绑定。 他们是天生的一对,爱情不是你想买,想买就能买的,必要的物质是少不了的,奋斗吧少年。 所以,如果你写了一个类,需要使用到行为,那么就果断地继承自yii\base\component。
同时,行为单独靠behavior一方是实现不了的,就好像爱情不是一厢情愿。 为了支持behavior,yii对于yii\base\component也进行了精心设计,这两者共同配合,才有了神奇的行为。
使用行为
一个绑定了行为的类,表现起来是这样的:
// step 1: 定义一个将绑定行为的类 class myclass extends yii\base\component { // 空的 } // step 2: 定义一个行为类,他将绑定到myclass上 class mybehavior extends yii\base\behavior { // 行为的一个属性 public $property1 = 'this is property in mybehavior.'; // 行为的一个方法 public function method1() { return 'method in mybehavior is called.'; } } $myclass = new myclass(); $mybehavior = new mybehavior(); // step 3: 将行为绑定到类上 $myclass->attachbehavior('mybehavior', $mybehavior); // step 4: 访问行为中的属性和方法,就和访问类自身的属性和方法一样 echo $myclass->property1; echo $myclass->method1(); |
上面的代码你不用全都看懂,虽然你可能已经用脚趾头猜到了这些代码的意思, 但这里你只需要记住行为中的属性和方法可以被所绑定的类像访问自身的属性和方法一样直接访问就ok了。 代码中,$myclass是没有property1method()成员的。这俩是$mybehavior的成员。 但是,通过attachbehavior()将行为绑定到对象之后,$mycalss就好像练成了吸星大法、化功大法,表现的财大气粗,将别人的属性和方法都变成了自己的。
另外,从上面的代码中,你还要掌握使用行为的大致流程:
- 从yii\base\component派生自己的类,以便使用行为;
- 从yii\base\behavior派生自己的行为类,里面定义行为涉及到的属性、方法;
- 将component和behavior绑定起来;
- 像使用component自身的属性和方法一样,尽情使用行为中定义的属性和方法。
行为的要素
我们提到了行为只是yii\base\behavior类的实例。 那么这个类究竟有什么秘密呢?其实说破了也没有什么的他只是一个简单的封装而已,非常的简单:
class behavior extends object { // 指向行为本身所绑定的component对象 public $owner; // behavior 基类本身没用,主要是子类使用,重载这个函数返回一个数组表 // 示行为所关联的事件 public function events() { return []; } // 绑定行为到 $owner public function attach($owner) { ... ... } // 解除绑定 public function detach() { ... ... } } |
这就是behavior的全部代码了,是不是很简单?behavior类的要素的确很简单:
- $owner成员变量,用于指向行为的依附对象;
- events()用于表示行为所有要响应的事件;
- attach()用于将行为与component绑定起来;
- deatch()用于将行为从component上解除。
下面分别进行讲解。
行为的依附对象
yii\base\behavior::$owner指向的是behavior实例本身所依附的对象。这是行为中引用所依附对象的唯一手段了。 通过这个$owner,行为才能访问所依附的component,才能将本身的方法作为事件handler绑定到component上。
$owner由yii\base\behavior::attach()进行赋值。 也就是在将行为绑定到某个component时,$owner就已经名花有主了。 一般情况下,不需要你自己手动去指定$owner的值, 在调用yii\base\componet::attachbehavior()将行为与对象绑定时, component会自动地将$this作为参数,调用yii\base\behavior::attach()。
有一点需要格外注意,由于行为从本质来讲是一个php类,其方法就是类方法,就是成员函数。 所以,在行为的方法中,$this引用的是行为本身, 试图通过$this来访问行为所依附的component是行不通的。 正确的方法是通过yii\base\behavior::$owner来访问component。
行为所要响应的事件
行为与事件结合后,可以在不对类作修改的情况下,补充类在事件触发后的各种不同反应。 为此,只需要重载yii\base\behavior::events()方法,表示这个行为将对类的何种事件进行何种反馈即可:
namespace app\components; use yii\db\activerecord; use yii\base\behavior; class mybehavior extends behavior { // 重载events() 使得在事件触发时,调用行为中的一些方法 public function events() { // 在event_before_validate事件触发时,调用成员函数 beforevalidate return [ activerecord::event_before_validate => 'beforevalidate', ]; } // 注意beforevalidate 是行为的成员函数,而不是绑定的类的成员函数。 // 还要注意,这个函数的签名,要满足事件handler的要求。 public function beforevalidate($event) { // ... } } |
上面的代码中,events()返回一个数组,表示所要做出响应的事件, 上例中的事件是activerecord::event_before_validate,以数组的键来表示, 而数组的值则表示做好反应的事件handler,上例中是beforevalidate(),事件handler可以是以下形式:
- 字符串,表示行为类的方法,如上面的例就是这种情况。 这个是与事件handler不同的,事件handler中使用字符串时,是表示php全局函数,而这里表示行为类内部的方法。
- 一个对象或类的成员函数,以数组的形式,如[$object,'methodname']。这个与事件handler是一致的。
- 一个匿名函数。
对于事件响应函数的签名,要求与事件handler一样:
function ($event) { }
具体内容,请参考事件(event)的内容。
行为的绑定与解除
说到绑定与解除,这意味着这个事情有2方,行为和component。单独一方是没有绑定或解除的说法的。 因此,这里我们先卖一关子,等后面讲绑定和解除的原理时,再来讲有关的内容。
这里你只需要知道,对于绑定和解除,behavior 分别使用attach()和detach()来实现就ok了。
定义一个行为
定义一个行为,就是准备好要注入到现有类中去的属性和方法, 这些属性和方法要写到一个yii\base\behavior类中。 所以,定义一个行为,就是写一个 behavior的子类,子类中包含了所要注入的属性和方法:
namespace app\components; use yii\base\behavior; class mybehavior extends behavior { public $prop1; private $_prop2; private $_prop3; private $_prop4; public function getprop2() { return $this->_prop2; } public function setprop3($value) { $this->_prop3 = $value; } public function foo() { // ... } protected function bar() { // ... } } |
上面的代码通过定义一个app\components\mybehavior类而定义一个行为。 由于 mybehavior 继承自yii\base\behavior从而间接地继承自yii\base\object。 没错,这是我们的老朋友了。因此,这个类有一个public的成员变量prop1, 一个只读属性prop2,一个只写属性prop3,一个public的方法foo()。 另外,还有一个private 的成员变量$_prop4,一个protected 的方法bar()。 如果你不清楚只读属性和只写属性,最好回头看看属性(property)部分的内容。
当这mybehavior与一个component绑定后, 绑定的component也就拥有了prop1prop2这两个属性和方法foo(),因为他们都是 public 的。 而 private 的$_prop4和 protected 的bar就得不到了。 至于原因么,后面讲行为注入的原理时,我们再解释。
行为的绑定
行为的绑定通常是由component来发起。有两种方式可以将一个behavior绑定到一个yii\base\component。 一种是静态的方法,另一种是动态的。静态的方法在实践中用得比较多一些。 因为一般情况下,在你的代码没跑起来之前,一个类应当具有何种行为,是确定的。 动态绑定的方法主要是提供了更灵活的方式,但实际使用中并不多见。
静态方法绑定行为
静态绑定行为,只需要重载yii\base\component::behaviors()就可以了。 这个方法用于描述类所具有的行为。如何描述呢? 使用配置来描述,可以是behavior类名,也可以是behavior类的配置数组:
namespace app\models; use yii\db\activerecord; use app\components\mybehavior; class user extends activerecord { public function behaviors() { return [ // 匿名的行为,仅直接给出行为的类名称 mybehavior::classname(), // 名为mybehavior2的行为,也是仅给出行为的类名称 'mybehavior2' => mybehavior::classname(), // 匿名行为,给出了mybehavior类的配置数组 [ 'class' => mybehavior::classname(), 'prop1' => 'value1', 'prop3' => 'value3', ], // 名为mybehavior4的行为,也是给出了mybehavior类的配置数组 'mybehavior4' => [ 'class' => mybehavior::classname(), 'prop1' => 'value1', 'prop3' => 'value3', ] ]; } } |
还有一个静态的绑定办法,就是通过配置文件来绑定:
[ 'as mybehavior2' => mybehavior::classname(), 'as mybehavior3' => [ 'class' => mybehavior::classname(), 'prop1' => 'value1', 'prop3' => 'value3', ], ] |
动态方法绑定行为
动态绑定行为,需要调用yii\base\compoent::attachbehaviors():
$component->attachbehaviors([ 'mybehavior1' => new mybehavior, // 这是一个命名行为 mybehavior::classname(), // 这是一个匿名行为 ]);
这个方法接受一个数组参数,参数的含义与上面静态绑定行为是一样一样的。
在上面的这些例子中,以数组的键作为行为的命名,而对于没有提供键名的行为,就是匿名行为。
对于命名的行为,可以调用yii\base\component::getbehavior()来取得这个绑定好的行为:
$behavior = $component->getbehavior('mybehavior2');
对于匿名的行为,则没有办法直接引用了。但是,可以获取所有的绑定好的行为:
$behaviors = $component->getbehaviors();
绑定的内部原理
只是重载一个yii\base\component::behaviors()就可以这么神奇地使用行为了? 这只是冰山的一角,实际上关系到绑定的过程,有关的方面有:
- yii\base\component::behaviors()
- yii\base\component::ensurebehaviors()
- yii\base\component::attachbehaviorinternal()
- yii\base\behavior::attach()
4个方法中,behavior只占其一,更多的代码,是在component中完成的。
yii\base\component::behaviors()上面讲静态方法绑定行为时已经提到了,就是返回一个数组用于描述行为。 那么yii\base\component::ensuerbehaviors()呢?
这个方法会在component的诸多地方调用__get()__set()__isset()__unset()__call()cangetproperty()hasmethod()haseventhandlers()on()off()等用到,看到这么多是不是头疼?一点都不复杂,一句话,只要涉及到类的属性、方法、事件这个函数都会被调用到。
这么众星拱月,被诸多凡人所需要的ensurebehaviors()究竟是何许人也? 就像名字所表明的,他的作用在于“ensure” 。其实只是确保behaviors()中所描述的行为已经进行了绑定而已:
public function ensurebehaviors() { // 为null表示尚未绑定 // 多说一句,为空数组表示没有绑定任何行为 if ($this->_behaviors === null) { $this->_behaviors = []; // 遍历 $this->behaviors() 返回的数组,并绑定 foreach ($this->behaviors() as $name => $behavior) { $this->attachbehaviorinternal($name, $behavior); } } } |
这个方法主要是对子类用的,yii\base\compoent没有任何预先注入的行为,所以,这个调用没有用。 但是对于子类,你可能重载了yii\base\compoent::behaviros()来预先注入一些行为。 那么,这个函数会将这些行为先注入进来。
从上面的代码中,自然就看到了接下来要说的第三个东东,yii\base\component\attachbehaviorinternal():
private function attachbehaviorinternal($name, $behavior) { // 不是 behavior 实例,说是只是类名、配置数组,那么就创建出来吧 if (!($behavior instanceof behavior)) { $behavior = yii::createobject($behavior); } // 匿名行为 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; } return $behavior; } |
首先要注意到,这是一个private成员。其实在yii中,所有后缀为*internal的方法,都是私有的。 这个方法干了这么几件事:
- 如果$behavior参数并非是一个 behavior 实例,就以之为参数,用yii::createobject()创建出来。
- 如果以匿名行为的形式绑定行为,那么直接将行为附加在这个类上。
- 如果是命名行为,先看看是否有同名的行为已经绑定在这个类上,如果有,用后来的行为取代之前的行为。
在yii\base\component::attachbehaviorinternal()中, 以$this为参数调用了yii\base\behavior::attach()。 从而,引出了跟绑定相关的最后一个家伙yii\base\behavior::attach(), 这也是前面我们讲行为的要素时没讲完的。先看看代码:
public function attach($owner) { $this->owner = $owner; foreach ($this->events() as $event => $handler) { $owner->on($event, is_string($handler) ? [$this, $handler] : $handler); } } |
上面的代码干了两件事:
- 设置好行为的$owner,使得行为可以访问、操作所依附的对象
- 遍历行为中的events()返回的数组,将准备响应的事件,通过所依附类的on()绑定到类上
说了这么多,关于绑定,做个小结:
- 绑定的动作从component发起;
- 静态绑定通过重载yii\base\componet::behaviors()实现;
- 动态绑定通过调用yii\base\component::attachbehaviors()实现;
- 行为还可以通过为 component 配置as配置项进行绑定;
- 行为有匿名行为和命名行为之分,区别在于绑定时是否给出命名。 命名行为可以通过其命名进行标识,从而有针对性地进行解除等操作;
- 绑定过程中,后绑定的行为会取代已经绑定的同名行为;
- 绑定的意义有两点,一是为行为设置$owner。二是将行为中拟响应的事件的handler绑定到类中去。
解除行为
解除行为只需调用yii\base\component::detachbehavior()就ok了:
$component->detachbehavior('mybehavior2');
这样就可以解除已经绑定好的名为mybehavior2的行为了。 但是,对于匿名行为,这个方法就无从下手了。不过我们可以一不做二不休,解除所有绑定好的行为:
$component->detachbehaviors();
这上面两种方法,都会调用到yii\base\behavior::detach(),其代码如下:
public function detach() { // 这得是个名花有主的行为才有解除一说 if ($this->owner) { // 遍历行为定义的事件,一一解除 foreach ($this->events() as $event => $handler) { $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler); } $this->owner = null; } } |
与yii\base\behavior::attach()相反,解除的过程就是干两件事: 一是将$owner设置为null,表示这个行为没有依附到任何类上。 二是通过component的off()将绑定到类上的事件hanlder解除下来。一句话,善始善终。
行为响应的事件实例
上面的绑定和解除过程,我们看到yii费了那么大劲,主要就是为了将行为中的事件handler绑定到类中去。 在实际时,行为用得最多的,也是对于compoent各种事件的响应。 通过行为注入,可以在不修改现有类的代码的情况下,更改、扩展类对于事件的响应和支持。 使用这个技巧,可以玩出很炫的花样。 而要将行为与component的事件关联起来,就要通过yii\base\behavior::events()方法。
上面behavior基类的代码中,这个方法只是返回了一个空数组,说明不对所依附的compoent的任何事件产生关联。 但是在实际使用时,往往通过重载这个方法来告诉yii,这个行为将对compoent的何种事件,使用哪个方法进行处理。
比如,yii自带的yii\behaviors\attributebehavior类,定义了在一个 activerecord 对象的某些事件发生时, 自动对某些字段进行修改的行为。 他有一个很常用的子类yii\behaviors\timestampbehavior用于将指定的字段设置为一个当前的时间戳。 常用于表示最后修改日期、上次登陆时间等场景。我们以这个行为为例,来分析行为响应事件的原理。
在yii\behaviors\attributebehavior::event()中,代码如下:
public function events() { return array_fill_keys(array_keys($this->attributes), 'evaluateattributes'); } |
这段代码的意思这里不作过多深入,学有余力的读者朋友可以自行研究,难度并不高。 这里,你只需要大致知道,这段代码将返回一个数组,其键值为$this->attributes数组的键值, 数组元素的值为成员函数evaluateattributes。
而在yii\behaviors\timestampbehavior::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的初始化部分。 结合上面2个方法的代码,对于yii\base\behavior::events()的返回数组,其格式应该是这样的:
return [ baseactiverecord::event_before_insert => 'evaluateattributes', baseactiverecord::event_before_update => 'evaluateattributes', ];
数组的键值用于指定要响应的事件, 这里是baseactiverecord::event_before_insert和baseactiverecord::event_before_update。 数组的值是一个事件handler,如上面的evaluateattributes。
那么一旦timestampbehavior与某个activerecord绑定,就会调用yii\behaviors\timestampbehavior::attach(), 那么就会有:
// 这里 $owner 是某个 activerecord public function attach($owner) { $this->owner = $owner; // 遍历上面提到的 events() 所定义的数组 foreach ($this->events() as $event => $handler) { // 调用 activerecord::on 来绑定事件 // 这里 $handler 为字符串 `evaluateattributes` // 因此,相当于调用 on(baseactiverecord::event_before_insert, // [$this, 'evaluateattributes']) $owner->on($event, is_string($handler) ? [$this, $handler] : $handler); } } |
因此,事件baseactiverecord::event_before_insert和baseactiverecord::event_before_update就绑定到了activerecord上了。当新建记录或更新记录时,timestampbehavior::evaluateattributes就会被触发。 从而实现时间戳的功能。具体可以看看yii\behaviors\attributebehavior::evaluateattributes()和yii\behaviors\timestampbehavior::getvalues()的代码。这里因为只是具体功能实现,对于行为的理解关系不大。 就不把代码粘出来占用篇幅了。
行为的属性和方法注入原理
上面我们了解到了行为的用意在于将自身的属性和方法注入给所依附的类。 那么yii中是如何将一个行为yii\base\behavior的属性和方法, 注入到一个yii\base\component中的呢? 对于属性而言,是通过__get()和__set()魔术方法来实现的。 对于方法,是通过__call()方法。
属性的注入
以读取为例,如果访问$component->property1,yii在幕后干了些什么呢? 这个看看yii\base\component::__get()
public function __get($name) { $getter = 'get' . $name; if (method_exists($this, $getter)) { return $this->$getter(); } else { // 注意这个 else 分支的内容,正是与 yii\base\object::__get() 的 // 不同之处 $this->ensurebehaviors(); foreach ($this->_behaviors as $behavior) { if ($behavior->cangetproperty($name)) { // 属性在行为中须为 public。否则不可能通过下面的形式访问呀。 return $behavior->$name; } } } if (method_exists($this, 'set' . $name)) { throw new invalidcallexception('getting write-only property: ' . get_class($this) . '::' . $name); } else { throw new unknownpropertyexception('getting unknown property: ' . get_class($this) . '::' . $name); } } |
重点来看yii\base\compoent::__get()与yii\base\object::__get()的不同之处。 就是在于对于未定义getter函数之后的处理,yii\base\object是直接抛出异常, 告诉你想要访问的属性不存在之类。 但是yii\base\component则是在不存在getter之后,还要看看是不是注入的行为的属性:
- 首先,调用了$this->ensurebehaviors()。这个方法已经在前面讲过了,主要是确保行为已经绑定。
- 在确保行为已经绑定后,开始遍历$this->_behaviors。 yii将类所有绑定的行为都保存在yii\base\compoent::$_behaviors[]数组中。
- 最后,通过行为的cangetproperty()判断这个属性, 是否是所绑定行为的可读属性,如果是,就返回这个行为的这个属性$behavior->name。 完成属性的读取。 至于cangetproperty()已经在 :ref::property部分已经简单讲过了, 后面还会有针对性地一个介绍。
对于setter,代码类似,这里就不占用篇幅了。
方法的注入
与属性的注入通过__get()__set()魔术方法类似, yii通过__call()魔术方法实现对行为中方法的注入:
public function __call($name, $params) { $this->ensurebehaviors(); foreach ($this->_behaviors as $object) { if ($object->hasmethod($name)) { return call_user_func_array([$object, $name], $params); } } throw new unknownmethodexception('calling unknown method: ' . get_class($this) . "::$name()"); } |
从上面的代码中可以看出,yii还是先是调用了$this->ensurebehaviors()确保行为已经绑定。
然后,也是遍历yii\base\component::$_behaviros[]数组。 通过hasmethod()方法判断方法是否存在。 如果所绑定的行为中要调用的方法存在,则使用php的call_user_func_array()调用之。 至于hasmethod()方法,我们后面再讲。
注入属性与方法的访问控制
在前面我们针对行为中public和private、protected的成员在所绑定的类中是否可访问举出了具体例子。 这里我们从代码层面解析原因。
在上面的内容,我们知道,一个属性可不可访问,主要看行为的cangetproperty()和cansetproperty()。 而一个方法可不可调用,主要看行为的hasmethod()。 由于yii\base\behavior继承自我们的老朋友yii\base\object,所以上面提到的三个判断方法, 事实上代码都在 object 中。我们一个一个来看:
public function cangetproperty($name, $checkvars = true) { return method_exists($this, 'get' . $name) || $checkvars && property_exists($this, $name); } public function cansetproperty($name, $checkvars = true) { return method_exists($this, 'set' . $name) || $checkvars && property_exists($this, $name); } public function hasmethod($name) { return method_exists($this, $name); } |
这三个方法真的谈不上复杂。对此,我们可以得出以下结论:
- 当向component绑定的行为读取(写入)一个属性时,如果行为为该属性定义了一个getter (setter),则可以访问。 或者,如果行为确实具有该成员变量即可通过上面的判断,此时,该成员变量可为 public, private, protected。 但最终只有 public 的成员变量才能正确访问。原因在上面讲注入的原理时已经交待了。
- 当调用component绑定的行为的一个方法时,如果行为已经定义了该方法,即可通过上面的判断。 此时,这个方法可以为 public, private, protected。 但最终只有 public 的方法才能正确调用。如果你理解了上一款的原因,那么这里也就理解了。
行为与继承和特性(traits) 的区别
从实现的效果看,你是不是会认为yii真是多此一举?php中要达到这样的效果,可以使用继承呀,可以使用php新引入的特性(traits)呀。但是,行为具有继承和特性所没有的优点,从实际使用的角度讲,继承和特性更靠底层点。靠底层,就意味着开发效率低,运行效率高。行为的引入,是以可以接受的运行效率牺牲为成本,谋取开发效率大提升的一笔买卖。
行为与继承
首先来讲,拿行为与继承比较,从逻辑上是不对的,这两者是在完全不同的层面上的事物,是不对等的。之所以进行比较,是因为在实现的效果上,两者有的类似的地方。看起来,行为和继承都可以使一个类具有另一个类的属性和方法,从而达到扩充类的功能的目的。
相比较于使用继承的方式来扩充类功能,使用行为的方式,一是不必对现有类进行修改,二是php不支持多继承,但是yii可以绑定多个行为,从而达到类似多继承的效果。
反过来,行为是绝对无法替代继承的。亚洲人,美洲人都是地球人,你可以将亚洲人和美洲人当成地球人来对待。但是,你绝对不能把一只在某些方面表现得像人的猴子,真的当成人来对待。
这里就不展开讲了。从本质上来讲,行为只是一种设计模式,是解决问题的方法学。继承则是php作为编程语言所提供的特性,根本不在一个层次上。
行为与特性
特性是php5.4之后引入的一个新feature。从实现效果看,行为与特性都达到把自身的public 变量、属性、方法注入到当前类中去的目的。在使用上,他们也各有所长,但总的原则可以按下面的提示进行把握。
倾向于使用行为的情况:
- 行为从本质上讲,也是php的类,因此一个行为可以继承自另一个行为,从而实现代码的复用。而特性只是php的一种语法,效果上类似于把特性的代码导入到了类中从而实现代码的注入,特性是不支持继承的。
- 行为可以动态地绑定、解除,而不必要对类进行修改。但是特性必须在类在使用use语句,要解除特性时,则要删除这个语句。换句话说,需要对类进行修改。
- 行为还以在在配置阶段进行绑定,特性就不行了。
- 行为可以用于对事件进行反馈,而特性不行。
- 当出现命名冲突时,行为会自行排除冲突,自动使用先绑定的行为。而特性在发生冲突时,需要人为干预,修改发生冲突的变量名、属性名、方法名。
倾向于使用特性的情况:
- 特性比行为在效率上要高一点,因为行为其实是类的实例,需要时间和空间进行分配。
- 特性是php的语法,因此,ide的支持要好一些。目前还没有ide能支持行为。