使用行为(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();//依次检查各个行为中是否有可写属性$nameforeach ($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. G6 图可视化引擎——入门教程——图的交互 Behavior

    G6 封装了一系列交互方法,方便用户直接使用.本文将为 Tutorial 案例 增加简单的交互:hover 节点.点击节点.点击边.放缩画布.拖拽画布.本节目标效果如下: 基本概念 交互行为 Beha ...

  2. MBSE建模学习之三:系统功能--行为(Behavior)的说明

    目录 "行为"(Behavior)的概念 "行为"(Behavior)的语法 行为和模块的关系 MBSE的模型中,行为(Behavior)是一个重要的概念.前面 ...

  3. 为Navigation 2创建自定义behavior tree plugin

    系列文章目录 思岚激光雷达rplidar从ROS 1到ROS 2的移植 ROS 2下navigation 2 stack的构建 订阅rviz2的导航目标位置消息"/goal_pose&quo ...

  4. behavior php,YII2框架中behavior行为的理解与使用方法示例

    本文实例讲述了YII2框架中behavior行为的理解与使用方法.分享给大家供大家参考,具体如下: YII2中的行为说白了就是对组件功能的扩展,在不改变继承关系的条件下. 行为附加到组件后,行为将注入 ...

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

    行为树(Behavior Tree)实践(1)– 基本概念 http://www.aisharing.com/archives/90 自从开博以来,每天都会关心一下博客的访问情况,看到一些朋友的订阅或 ...

  6. Yii2 behavior运用

    1 class ReturnDataTypeBehaviors extends Behavior 2 { 3 4 public $type = 'json'; 5 public $pcOrMobile ...

  7. yii2 behavior 排除某个方法_中学科目二 | 教学方法

    重点梳理01教学方法 选择/简答 ▎知识讲解  课后练习  例题1:教师按一定的教学要求向学生提出问题,要求学生回答,并通过问答的形式来引导学生获取或巩固知识的方法,称为( ). A. 讨论法 B. ...

  8. 深入理解YII2.0

    接触使用YII已经有几年光阴了,现在结合digpage.com(解析的很深入)这个网站的内容,会有变动,稍微记录一下. 一来帮助自己梳理一遍,二来做个一份备份入口.详细的还是请访问 (侵·删·联QQ2 ...

  9. 属性-Yii 基础-深入理解YII2.0(1.1)

    属性(Property) 属性用于表征类的状态,从访问的形式上看,属性与成员变量没有区别. 你能一眼看出 $object->foo 中的 foo 是成员变量还是属性么?显然不行. 但是,成员变量 ...

  10. Yii2.0 RESTful API 之版本控制

    Yii2.0 RESTful API 之版本控制 之前我写过两篇关于 Yii2.0 RESTful API 如何搭建,以及 认证 等处理,但是没有涉及到版本管理,今天就来谈谈版本管理如何实现. 索性就 ...

最新文章

  1. iOS-响应上下左右滑动手势
  2. Selenium3自动化测试——2. python编写简单自测代码
  3. 【杂文】企业数字化转型展望—角色转型
  4. libnet TCP示例
  5. JMeter 性能测试进阶实战
  6. 创建支持依赖注入、Serilog 日志和 AppSettings 的 .NET 5 控制台应用
  7. antd react dva在model中使用另一个model的state值
  8. java 继承示例_Java中的继承类型以及示例
  9. java文件不能转class_安了jdk 却不能将.java文件转换成.class 文件,一运行就说‘javac’不是内部或外部指令,却能运行.class文...
  10. jQuery 学习笔记之二 (jQuery代码风格)
  11. centos7 mysql5.6.35_Centos7.2.1511 编译安装Mysql5.6.35
  12. C# 在word中查找及替换文本
  13. 网络通信的发展和基础原理
  14. winrar4.2 破解 注册码
  15. 迅捷pdf虚拟打印机怎么安装打印
  16. messagedigest 图片加密_MessageDigest来实现数据加密
  17. 对COM组件的调用返回了错误HRESULT E_FAIL
  18. python 中无限循环_Python中如何解决无限循环的问题
  19. 弘辽科技:6个核心关键词,抓住创业的“命运转折点”!
  20. flutter doctor --android-licenses

热门文章

  1. 计算机桌面不能显示器,电脑显示屏亮但是主机已开机无法显示桌面
  2. weight和weightSum的区别
  3. C语言数字图像处理进阶---6 LOMO滤镜
  4. 视频2-视频文件解析和格式说明
  5. 读债务危机0814-08年9月崩溃
  6. 计算机职业适应性测试题库,职业适应性测试题库 一、性格职业适应度测试.doc...
  7. 网页设计 颜色搭配
  8. java编写投票功能需求分析
  9. SRT编码器之Rendezvous模式详解
  10. 驱动器存在问题-U盘量产-主控SM3255AB