使用行为(behavior)可以在不修改现有类的情况下,对类的功能进行扩充。通过将行为绑定到一个类,可以使得类具有行为本身所具有的属性和方法,就好像是类本来就具有的这些属性和功能一样。

好的代码设计,必须要同时满足可复用性、可维护性和可扩展性。设计原则中有一条非常重要的一条:类应该对扩展开放,对修改关闭。改变原有代码往往会带来潜在风险,因此我们尽量减少修改的行为。我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可以搭配新的行为。如果能实现这样的目标,有什么好处呢?这样的设计具有弹性,可以应对改变,可以接收新的功能来应对改变的需求。

Yii的行为就是这样一类对象,当一个对象(继承了Component的)想要扩展功能,又不想改变原有代码时,那么你完全可以用行为去实现这些新功能,然后绑定到该对象上——完全是符合“开闭原则”的。

Yii的行为都需要继承自yii\base\Behavior,而能接受行为绑定从而扩充自身功能的只能是yii\base\Component的子类,只继承BaseObject基类没有继承Component的不能享受此“待遇”。因此,行为是组件才有的功能。行为和事件结合起来使用,还可以定义组件在何种事件进行何种反馈。因此行为有如下两个作用:

  1. 将属性和方法注入到一个component里面,被访问时和别的属性或者方法访问无异(行为的附加
  2. 响应component中触发的事件,以便对某一事件作出反应(行为的绑定和触发,是对事件的应用)

定义行为

行为必须继承自yii\base\Behavior,定义一个行为仿照下面进行:

class MyBehavior extends \yii\base\Behavior
{
public $prop1;
private $_prop2;
private $_prop3; //绑定事件和处理器,从而扩展类的功能表现,这里体现了“行为”字面意义
public function events()
{ } //行为的只读属性
public function getProp2()
{
return $this->_prop2;
} //行为的只写属性
public function setProp3($prop3)
{
$this->_prop3 = $prop3;
} //行为的方法
public function foo()
{
return 'foo';
} protected function bar()
{
return 'bar';
} }

接下来,将行为附加到对象上,从而扩充对象的功能:

$user = new User();
//$user对像附加行为,扩充功能
$user->attachBehavior('myBehavior', new MyBehavior());
//获取prop2属性
$user->prop2;
//给只读属性赋值会报错
$user->prop2 = 3;
//给只写属性prop3赋值
$user->prop3 = 2;
//操作可读-可写属性prop1
$user->prop1 = 1;
$var = $user->prop1; // 使用方法foo
$user->foo();
// 不可访问,这里会抛出'Unknown Method'异常
$user->bar();

当然MyBehavior()完全可以支持依赖注入,从而在运行时决定这些属性的值。

从上面可以看出,$user对象使用其MyBehavior的属性和方法来几乎毫不费劲,就像自己拥有这些属性和方法一样。但是,我们并没有给User类中添加任何一行代码,因此这个扩展做得真是悄无声息啊!

行为的附加

行为的附加或者绑定,通常是由Component来发起。有两种方式可以将一个Behavior绑定到一个 yii\base\Component 。 一种是静态附加行为,另一种是动态附加行为。静态附加在实践中用得比较多一些,因为一般情况下,在你的代码没跑起来之前,一个类应当具有何种行为是确定的。 动态附加主要是提供了更灵活的方式,上面即是行为的动态附加,但实际使用中并不多见。

静态附加

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',
]
];
}
}

上面的数组响应的键就是行为的名称,这种行为成为命名行为,没有指定名称的就成为匿名行为

还有一个静态的绑定办法,就是通过配置文件来绑定:

[
'class' => User::className(),
'as myBehavior2' => MyBehavior::className(),
'as myBehavior3' => [
'class' => MyBehavior::className(),
'prop1' => 'value1',
'prop3' => 'value3',
],
]

通过这个配置文件获取的User对象的实例,依然被附加了MyBehavior行为。

动态附加

要动态附加行为,在对应组件里调用 yii\base\Component::attachBehavior() 方法即可,如:

use app\components\MyBehavior;
// 附加行为——对象
$user->attachBehavior('myBehavior1', new MyBehavior);
// 附加行为——类名
$user->attachBehavior('myBehavior2', MyBehavior::className());
// 附加行为——配置数组
$user->attachBehavior('myBehavior3', [
'class' => MyBehavior::className(),
'prop1' => 'value1',
'prop2' => 'value2',
]);

也可以通过 yii\base\Component::attachBehaviors()同时附加多个行为:

$myBehavior = new MyBehavior();
$user->attachBehaviors([
'myBehavior1'=> $myBehavior,
[
'class' => MyBehavior2::className(),
'prop1' => 'value1',
'prop3' => 'value2',
],
new MyBehavior3()
]);

附加多个行为,那么组件就获得了所有这些行为的属性和方法。

不管是静态附加还是动态附加,命名行为都可以通过yii\base\Component::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',
],
];
}

yii\base\Component::ensureBehaviors()方法经常出现,它的作用是将各种动态的和静态的方式附加的行为变成标准格式(参看$_behaviors的数据结构):

 public function ensureBehaviors()
{
if ($this->_behaviors === null) {
$this->_behaviors = [];
// behaviors()方法由Component的子类重写
foreach ($this->behaviors() as $name => $behavior) {
$this->attachBehaviorInternal($name, $behavior);
}
}
}

接下来的第三个出场的attachBehaviorInternal(),我们看看是何方神圣:

 private function attachBehaviorInternal($name, $behavior)
{
//如果是配置数组,那就将其创建出来再说
if (!($behavior instanceof Behavior)) {
$behavior = Yii::createObject($behavior);
}
if (is_int($name)) { // 匿名行为
//先是行为本身和component绑定
$behavior->attach($this);
//将行为放进$_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);
}
}

行为的解除

有附加当然有解除,命名行为可以被单个解除,使用方法Component::detachBehavior($name),匿名行为不可以单独解除,但是可使用detachBehaviors()方法解除所有的行为。

//解除命名行为
$user->detachBehavior('myBehavior1');
//解除所有行为
$user->detachBehaviors();

这上面两种方法,都会调用到 yii\base\Behavior::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;
}
}

行为所要响应的事件

行为与事件结合后,可以在不对类作修改的情况下,补充类在事件触发后的各种不同反应。因此,只需要重载 yii\base\Behavior::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) { ... }

这些方法中都会传递事件$event过来,通过$event你可以获得事件名,触发此事件的对象(类或者实例),附加的数据信息。详见《Yii2基本概念之——事件(Event)》。

行为响应事件的实例

Yii费了那么大劲,主要就是为了将行为中的事件handler绑定到类中去。因为在编程中用的最多的,也就是Component对各种事件的响应。通过行为注入,可以在不修改现有类的代码的情况下更改、扩展类对于事件的响应和支持。使用这个技巧,可以玩出很酷的花样出来。

比如,Yii自带的 yii\behaviors\AttributeBehavior 类,定义了在一个 ActiveRecord 对象的某些事件发生时, 自动对某些字段进行修改的行为。它有一个很常用的子类 yii\behaviors\TimeStampBehavior 用于将指定的字段设置为一个当前的时间戳。现在以它为例子说明行为的运用。

在 yii\behaviors\AttributeBehavior::event() 中,代码如下:

 public function events()
{
return array_fill_keys(
array_keys($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这个数组。结合前面的两个方法,返回的$event数组应该是这样的:


return [
BaseActiveRecord::EVENT_BEFORE_INSERT => 'evaluateAttributes',
BaseActiveRecord::EVENT_BEFORE_UPDATE => 'evaluateAttributes',
];

这里的意思是BaseActiveRecord::EVENT_BEFORE_INSERTBaseActiveRecord::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_atupdated_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自身的面向对象特性(重载,魔术方法,成员变量/函数可见性)和一些数据结构,外加巧妙的算法来实现的。因此“解剖”的目的就在于,解开这次神秘面纱,搞清楚内在逻辑,最终使得自己的编程能力得到切实的提高。

Yii2基本概念之——行为(Behavior)的更多相关文章

  1. Yii2基本概念之——属性(property)

    学习任何一门学问,往往都是从起基本的概念学起.万丈高楼平地起,这些基本概念就是高楼的基石,必须做详尽的分析.我们知道,Yii2是一款脉络清晰的框架,理顺了基础的概念和基本功能,学习更高级和复杂的功能就 ...

  2. Yii2的深入学习--行为Behavior

    我们先来看下行为在 Yii2 中的使用,如下内容摘自 Yii2中文文档 行为是 [[yii\base\Behavior]] 或其子类的实例.行为,也称为 mixins,可以无须改变类继承关系即可增强一 ...

  3. Yii2.0源码阅读-behavior的实现原理

    Yii2.0中的一个思想就是组件化的思想,所以.大多数的类都直接或间接的继承自yii\base\Component,而组件的三大功能:属性.事件.行为. 行为的目的是为了方便的扩展一个类的功能,而不需 ...

  4. Yii2基本概念之——配置(Configurations)

    在Yii中创建新对象或者初始化已经存在的对象广泛的使用配置,配置通常包含被创建对象的类名和一组将要赋值给对象的属性的初始值,这里的属性是Yii2的属性.还可以在对象的事件上绑定事件处理器,或者将行为附 ...

  5. Yii2基本概念之——生命周期(LifeCycle)

    人有生老病死,一年有春夏秋冬四季演替,封建王朝有兴盛.停滞.衰亡的周期律--"其兴也勃焉,其亡也忽焉".换句话说,人,季节,王朝等等这些世间万物都有自己的生命周期.同样地,在软件行 ...

  6. Yii2基本概念之——事件(Event)

    说起事件(event),我们可是一点都不陌生.现实生活当中的事件无处不在,比如你发了一条微博,触发了一条事件,导致关注你的人收到了一条消息,看到你发的内容:比如你通过支付宝买东西,付了款,触发一个事件 ...

  7. 深入解析Android Design包——Behavior

    已经说过了,在AndroidDesign包中主要有两个核心概念:一是NestedScroll,另一个就是Behavior. 相比于NestedScroll这个概念来说,Behavior分析起来会难很多 ...

  8. 行为树(Behavior Tree)实践(1)– 基本概念

    原文地址:http://www.360doc.com/content/15/0107/11/15099545_438831036.shtml 自从开博以来,每天都会关心一下博客的访问情况,看到一些朋友 ...

  9. YII2中behavior行为的理解与使用

    YII2中的行为说白了就是对组件功能的扩展,在不改变继承关系的条件下. 行为附加到组件后,行为将注入自已的方法和属性到组件,可以像组件访问自定义的方法和属性一样访问行为. 注意行为是对功能的扩展,不要 ...

随机推荐

  1. Redis-04.备份与恢复

    RDB(Redis DataBase) 在指定的时间间隔内将内存中的数据集快照写入磁盘,可以理解为Snapshot快照,它恢复时是将快照文件直接读到内存里. Redis会单独创建(fork)一个子进程 ...

  2. 手动模拟attach cinder volume的过程

    我们首先启动一台机器,启动的时候attach一个volume 创建一个空的cinder volume root:~# cinder create --display-name emptyvolume1 ...

  3. 第80节:Java中的MVC设计模式

    第80节:Java中的MVC设计模式 前言 了解java中的mvc模式.复习以及回顾! 事务,设置自动连接提交关闭. setAutoCommit(false); conn.commit(); conn ...

  4. 《http权威指南》读书笔记17

    概述 最近对http很感兴趣,于是开始看<http权威指南>.别人都说这本书有点老了,而且内容太多.我个人觉得这本书写的太好了,非常长知识,让你知道关于http的很多概念,不仅告诉你怎么做 ...

  5. 怎样在mybatis里向mysql中插入毫秒数的时间?

    由于业务场景需求,需要记录精准的时间,但是呢,又不要想使用int类型来存储时间,因为这样的可读性比较差了. 怎样在mybatis中向数据库插入毫秒级别的时间呢? 首先,先来看看怎样向数据库中插入毫秒时 ...

  6. dubbo实用知识点总结(二)

    1. 参数验证 2. 结果缓存 3. 泛化引用 客户端没有对应接口类的情况,可以直接调用 4. 泛化实现 5. 回声测试 用于检测服务是否可用 6. 上下文信息 7. 隐式传参(不常用) 8. 异步调 ...

  7. 关于动画的几种状态表示的含义以及能够使用2d动画表述为什么要使用3d动画表述

    transform 四种转换 translate 位置scale 缩放rotate 旋转skew 倾斜 以上四种转换方式是比较特殊的,其实他们都是由matrix 矩阵转换来: animation的五种 ...

  8. python基础-字符串(6)

    一.引言 当打来浏览器登录某些网站的时候,需要输入密码,浏览器把密码传送到服务器后,服务器会对密码进行验证,其验证过程是把之前保存的密码与本次传递过去的密码进行对比,如果相等,那么就认为密码正确,否则 ...

  9. remote: Incorrect username or password ( access token )

    解决问题 进入控制面板 用户账号,选择管理您的凭据 修改凭据 修改完成后,保存即可

  10. [EFCore]EntityFrameworkCore Code First 当中批量自定义列名

    在使用.NET CORE 进行 Web 开发的时候会考虑到使用不同数据库的情况,并且在每种数据库建立表结构的时候会采用不同的命名规则.之前的解决方法是使用 [ColumnAttribute] 或者 [ ...