什么是行为
所谓行为,其实说白了,就是把A类绑定到B类上,可以在不修改B类的情况下,对B类进行功能和属性的扩充,这样B类就拥有了A类的属性和方法,只是在这里,我们习惯成A类为行为。
举一个例子:
class MyClass
{
public $hello = "我是类的自有属性hello".PHP_EOL;
public function hello()
{
echo "我是类的自有方法hello".PHP_EOL;
}
}
class MyBehavior
{
public $hi = "我是行为的自有属性hi".PHP_EOL;
public function hi()
{
echo "我是行为的自有方法hi".PHP_EOL;
}
}
$class = new MyClass();
$behavior = new MyBehavior();
$class->attachBehavior('behavior1',$behavior);//将类和行为进行绑定,这里为$behavior绑定了一个别名:behavior1
echo $class->hello;//输出:我是类的自有属性hello
$class->hello();//输出:我是类的自有方法hello
echo $class->hi;//期望输出:我是行为的自有属性hi
$class->hi();//期望输出:我是行为的自有方法hi
实现自己的行为
实现行为的关键知识点:__get(),__set(),__call(),call_user_func_array,前三个是PHP的魔术方法,这四个是非常重要的知识点
以上伪代码就是行为的实现,现在我们来动手实现自己的行为:
//定义一个基类,后面的类都来继承它
class Base
{
//存放行为类的容器数组
private $_behaviors = [];
//绑定行为类:其实就是放入数组$_beheviors
public function attachBehavior($name,$behavior)
{
//如果$_beheviors里有这个别名的话,先unset掉,给新绑定的行为让路
if(isset($this->_behaviors[$name])) {
unset($this->_behaviors[$name]);
}
$this->_behaviors[$name] = $behavior;
}
public function __get($name)
{
foreach($this->_behaviors as $behavior) {
if($behavior->hasProperty($name)) {
return $behavior->$name;
}
}
}
public function __set($name,$value)
{
foreach($this->_behaviors as $behavior) {
if($behavior->hasProperty($name)) {
$behavior->$name = $value;
}
}
}
public function __call($method,$params)
{
foreach($this->_behaviors as $behavior) {
if($behavior->hasMethod($method)) {
call_user_func_array([$behavior,$method],$params);
}
}
}
}
//定义行为基类,所有的行为类都继承它
class Behavior
{
//判断该行为类是否有$name属性
public function hasProperty($name)
{
return property_exists($this,$name);
}
//判断该行为类是否有$method方法
public function hasMethod($method)
{
return method_exists($this,$method);
}
}
定义好这两个基类之后,我们还要修改一下MyClass和MyBehavior:
class MyClass extends Base
{
// 让Myclass继承自Base,才能绑定行为
···
}
class MyBehavior extends Behavior
{
// 让MyBehavior继承自Behavior
···
}
然后我们继续运行我们的代码:
$class = new MyClass();
$behavior = new MyBehavior();
$class->attachBehavior('behavior1',$behavior);
echo $class->hello;// 输出:我是类的自有属性hello
$class->hello();// 输出:我是类的自有方法hello
echo $class->hi;// 输出:我是行为的自有属性hi
$class->hi();// 输出:我是行为的自有方法hi
// 这里动态去设置行为的属性
$class->hi = "我是重新设置好的行为的自有属性hi".PHP_EOL;
echo $class->hi;// 输出:我是重新设置好的行为的自有属性hi
看到了吗,我们想要的期望输出,这回变成现实了,整体还是很好理解的。
这里没有考虑行为的方法和属性的权限问题,例子里都写成了Public,只是方便举例和测试,实际开发中肯定要进行权限控制判断的。
Yii中的行为
如何使用Yii中的行为
- 由于在Yii中,实现行为的功能是yii\base\Component中的,所以想实现行为,就必须从yii\base\Component继承;
- 从yii\base\Behavior派生自己的行为类;
- 将Component和Behavior绑定起来;
- 像使用Component自己的属性和方法一样,使用绑定好的行为中的属性和方法;
行为的绑定
在Yii中,一个行为的绑定实现步骤是这样的:
* yii\base\Component::behaviors(); //第一步
* yii\base\Component::ensureBehaviors(); //第二步
* yii\base\Behavior::attach(); //第三步
在yii项目实际的开发中,我们很少手动去绑定行为,一般都是定义一个数组,如下所示:
class MyClass
{
// 定义好的要绑定的行为数组
public function behaviors()
{
return [
// 匿名的行为,仅给出行为的类名
MyBehavior::className(),
// 名为myBehavior2的行为,也是仅给出行为的类名
'myBehavior2' => MyBehavior::className(),
// 匿名行为,给出了MyBehavior类的配置数组
[
'class' => MyBehavior::className(),
],
];
}
}
那么,yii是如何去绑定上述定义好的数组的呢?答案在第二步,yii\base\Component::ensureBehaviors():
public function ensureBehaviors()
{
if ($this->_behaviors === null) {
$this->_behaviors = [];
// 遍历 $this->behaviors()数组,并绑定
foreach ($this->behaviors() as $name => $behavior) {
$this->attachBehaviorInternal($name, $behavior);
}
}
}
ensureBehaviors()方法其实是调用了attachBehaviorInternal():
private function attachBehaviorInternal($name, $behavior)
{
// 如果不是Behavior的实例,比如说只是一个类名的字符串,或者一个配置数组
if (!($behavior instanceof Behavior)) {
// 就把这个对象创建出来
// createObject()人如其名,就是用来创建对象的,这个知识点在Yii的服务定位器中,其实就是去容器里取出来对象
$behavior = Yii::createObject($behavior);
}
// 如果是int,说明在配置$behaviors数组的时候,没有对其人为的设定一个key,那肯定就是默认的数字索引
if (is_int($name)) {
$behavior->attach($this); // 这里是第四步
$this->_behaviors[] = $behavior; //将行为放入$this->_behaviors容器
} else {
// 命名行为:
// 已经有一个同名的行为,要先解除,再将新的行为绑定上去。
if (isset($this->_behaviors[$name])) {
$this->_behaviors[$name]->detach();
}
$behavior->attach($this); // 这里是第四步
$this->_behaviors[$name] = $behavior; //将行为放入$this->_behaviors容器
}
return $behavior;
}
其实经过以上两步,Yii已经将行为绑定到Component的子类中,这个时候,已经可以使用行为的属性和方法了,那么第三步是干什么的呢,我们先看一下源码,代码在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);
}
}
这个attach方法主要是为了把行为中定义的事件都绑定到行为的所有者身上,这样所有者就有了触发事件的能力
行为的解绑
行为的解绑的原理和绑定刚好相反,从行为数组$_behaviors中,unset掉要解绑的行为,代码在yii\base\Component::detachBehavior():
public function detachBehavior($name)
{
$this->ensureBehaviors();
if (isset($this->_behaviors[$name])) {
$behavior = $this->_behaviors[$name];
unset($this->_behaviors[$name]);
$behavior->detach(); // 这个方法主要是为了解绑通过行为绑定给Component的事件
return $behavior;
}
return null;
}
$behavior->detach()方法在yii\base\Behavior::detach():
public function detach()
{
if ($this->owner) {
foreach ($this->events() as $event => $handler) {
// 解除Component的事件
$this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler);
}
$this->owner = null;
}
}
行为的属性和方法注入的原理分析
yii的行为实现,其实和我们文章一开始自己写的行为类实现原理是一样的,主要用到了__get(),__set(),__call()魔术方法:
属性的获取:
public function __get($name)
{
// 由于在yii的基类Object里为属性定义了getter()方法
// 所以获取属性的时候,先判断该属性有没有对应的getter()方法
$getter = 'get' . $name;
if (method_exists($this, $getter)) {
// read property, e.g. getName()
return $this->$getter();
}
// 如果没有getter(),那么该属性就可能是行为的属性
// 所以遍历行为数组,如果哪个行为有这个属性,就返回
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) {
return $behavior->$name;
}
}
···
}
属性的设置:
public function __set($name, $value)
{
// 设置一个属性的时候,如果有setter()方法,那么优先调用setter
$setter = 'set' . $name;
if (method_exists($this, $setter)) {
$this->$setter($value);
return;
}
$this->ensureBehaviors();
// 遍历行为数组,如果行为有$name属性,那么就设置为$value
foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name)) {
$behavior->$name = $value;
return;
}
}
···
}
方法的获取:
public function __call($name, $params)
{
$this->ensureBehaviors();
// 遍历行为数组
foreach ($this->_behaviors as $object) {
// 如果某个行为有$name这个方法体,那么就调用,参数为$params
if ($object->hasMethod($name)) {
return call_user_func_array([$object, $name], $params);
}
}
···
}
引出:行为和Traits的区别
php的traits是在5.4版本之后才引入的一个新特性,从实现效果上来看,行为和traits都能把自身的属性和方法注入到当前类中去,但是还是有一些区别:
- 行为从本质上来讲,也是PHP的类,一个行为可以继承自另一个行为,从而实现代码的复用。而traits只是php的一种语法,效果上类似把tratis的代码导入到一个类中,从而实现代码的注入,特性是不支持继承的。
- 行为可以支持动态的绑定,解绑,而不必对类进行修改。但是traits必须在类内使用use,才能导入,要解除traits时,则要删除这个use语句,也就是说要对类进行修改。
- traits在效率上要比行为高一点,因为traits是php内部实现的,行为是我们后期自己实现的。