浅析Yii20的行为Behavior

45次阅读

共计 5740 个字符,预计需要花费 15 分钟才能阅读完成。

概念理解:使用行为(behavior)可以在不修改现有类的情况下,对类的功能进行扩充。通过将行为绑定到一个类,可以使类具有行为本身所定义的属性和方法,就好像类本来就有这些属性和方法一样。而不需要写一个新的类去继承或包含现有类。在功能上类似于 Traits,达到类似于多继承的目的。

行为的实现 demo

<?php

namespace common\components;


use yii\base\Component;

// 待绑定行为的类
class MyClass extends Component
{ }


<?php


namespace common\components;


use yii\base\Behavior;

// 定义一个行为类
class BehaviorTest extends Behavior
{
    const EVENT_AFTER_SAVE = 'eventAfterAttach';

    public $_val = '我是 BehaviorTest 里面的公有属性_val';

    public function getOutput()
    {echo '我是 BehaviorTest 里面的公有方法 getOutput';}

    public function events()
    {
        return [self::EVENT_AFTER_SAVE => 'afterAttach'];
    }

    public function afterAttach()
    {echo '事件已触发';}
}

// 在控制器或者命令行下调用

$myClass = new MyClass();

$myBehavior = new BehaviorTest();

// 将行为绑定到 MyClass 的实例
$myClass->attachBehavior('test', $myBehavior);

// MyClass 实例调用行为类中的属性
echo $myClass->_val;

// MyClass 实例调用行为类中的方法
$myClass->getOutput();

// MyClass 实例触发行为类中定义的事件
$myClass->trigger(BehaviorTest::EVENT_AFTER_SAVE);

行为的绑定原理

我们先来看一下 $myClass->attachBehavior('test', $myBehavior); 行为绑定的时候做了哪些事情?好的,又是我们的老朋友 yii\base\Component

// yii\base\Component 的部分代码

private $_behaviors;

public function behaviors()
{return [];
}

public function attachBehavior($name, $behavior)
{$this->ensureBehaviors();
        return $this->attachBehaviorInternal($name, $behavior);
}

public function ensureBehaviors()
{if ($this->_behaviors === null) {$this->_behaviors = [];
            foreach ($this->behaviors() as $name => $behavior) {$this->attachBehaviorInternal($name, $behavior);
            }
        }
}

private function attachBehaviorInternal($name, $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;
}

$myClass 调用 attachBehavior() 传入俩个参数,一个是行为名称,另一个是行为类的名称或实例或者是数组,接着 attachBehavior() 调用了 ensureBehaviors(),这个函数我们暂时用不到,因为我们没有在 MyClass 里面重载 behaviors(),不过也能大概猜到ensureBehaviors() 的用途了。再往下调用的是私有函数 attachBehaviorInternal(),这个函数先判断传进来的$behavior 是否已实例化,如果还没有则进行实例化,再通过$name 判断是匿名行为还是命名行为,如果是命名行为,需要查看是否已经绑定同名的行为,如果绑定了同名的行为,会将以前的行为先解绑再调用 $behavior->attach($this);[注:这里$this 指的 MyClass 实例,也就是 $myClass],这样我们就来到了yii\base\Behaviorattach()方法,下面是 attach() 方法的源码:

public function attach($owner)
{
    $this->owner = $owner;
    foreach ($this->events() as $event => $handler) {$owner->on($event, is_string($handler) ? [$this, $handler] : $handler);
    }
}

$this->owner = $owner;[注:这里 $this 指的是$behavior 也就是类 BehaviorTest 的实例],这句代码指定了行为类的主人是谁,后面的代码看着似乎似曾相识?是的,就是将行为类 events() 方法里面的事件也绑定的宿主身上,这里不具体展开,有兴趣的小伙伴可以看一下浅析 Yii2.0 的事件 Event。最后将行为名称和行为实例放到 $myClass 的属性 _behavior 中,至此,行为的绑定就结束了。好像也没干什么啊,我们现在可以打印一下 $myClass 的数据结构是怎样的?

common\components\MyClass Object
([_events:yii\base\Component:private] => Array
        ([eventAfterAttach] => Array
                ([0] => Array
                        ([0] => Array
                                ([0] => common\components\BehaviorTest Object
                                        ([_val] => 我是 BehaviorTest 里面的公有属性_val
                                            [hello:common\components\BehaviorTest:private] => 我是 BehaviorTest 里面的私有属性 hello
                                            [owner] => common\components\MyClass Object
 *RECURSION*
                                        )

                                    [1] => afterAttach
                                )

                            [1] => 
                        )

                )

        )

    [_eventWildcards:yii\base\Component:private] => Array
        ( )

    [_behaviors:yii\base\Component:private] => Array
        ([test] => common\components\BehaviorTest Object
                ([_val] => 我是 BehaviorTest 里面的公有属性_val
                    [hello:common\components\BehaviorTest:private] => 我是 BehaviorTest 里面的私有属性 hello
                    [owner] => common\components\MyClass Object
 *RECURSION*
                )

        )

)

可以看到 $myClass 已经绑定了一个行为test,绑定了一个事件eventAfterAttach,那么绑定行为以后,是怎么调用行为类里面的属性和方法呢?

行为的使用原理

还是看一下 demo 里面 $myClass->_val 这句代码是怎么执行的?根据上面 $myClass 的数据结构可以看出并没有 _val 这个属性,但是 yii\base\Component 里面实现了 __get() 这个魔术方法,我们看一下源码。

public function __get($name)
{
    $getter = 'get' . $name;
    if (method_exists($this, $getter)) {// read property, e.g. getName()
        return $this->$getter();}

    // behavior property
    $this->ensureBehaviors();
    foreach ($this->_behaviors as $behavior) {if ($behavior->canGetProperty($name)) {return $behavior->$name;}
    }

    if (method_exists($this, 'set' . $name)) {throw new InvalidCallException('Getting write-only property:' . get_class($this) . '::' . $name);
    }

    throw new UnknownPropertyException('Getting unknown property:' . get_class($this) . '::' . $name);
}

又似曾相识?是的,跟 yii\base\BaseObject 里面属性的实现类似,有兴趣的小伙伴可以看一下浅析 Yii2.0 的属性 Property。然后直接看注释 behavior property 部分,又去调用了 ensureBehaviors(),先不管,接着又去遍历_behaviors 这个属性,根据上面 $myClass 的数据结构得知,此时 foreach 里面的 $behavior 就是行为类 common\components\BehaviorTest 实例,先通过 canGetProperty 判断 _val 是否可读或者存在,大家可以去看 yii\base\BaseObject 里面该方法的实现。我们这里返回的是 true,然后就直接通过common\components\BehaviorTest 的实例 $behavior 返回 _val 的值。

根据上面获取行为类里面属性的流程我们注意到:

  1. 因为是通过实例化行为类去调用的属性,所以属性是 protected 或者是 private 是获取不到的。
  2. 如果 Component 绑定了多个行为,并且多个行为中有同名的属性,那么该 Component 获取的是第一个行为类里面的该属性。

那么行为类里面的方法是怎么被调用的呢?属性的调用是通过 __get() 来实现的,很容易联想到方法的调用是通过 __call() 来实现的,我们查看一下 yii\base\BaseObject 源码,果然里面实现 __call() 这个魔术方法,下面是源码,然后对照上面 $myClass 的数据结构一看就明白了。需要注意的是,跟上面属性的调用一样,方法也必须是 public 的,protected、private方法是调用不了的。

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()");
}

注意到 __call() 里面又有一个老朋友 ensureBehaviors() 这个函数似乎无处不在?是的,查看一下 yii\base\Component 里面的源码我们可以发现所有的公有方法都调用了这个函数,那么这个函数到底是干嘛的呢,其实 demo 里面绑定行为的方式可以称为主动绑定,就是我们主动调用函数 attachBehavior() 去绑定行为的,对应的就是被动绑定了,实现方式就是在待绑定行为的类里面重载 behaviors() 这个函数就可以实现绑定了,相当于一个行为的配置项。俩种绑定方式看个人喜好了,如果一个类需要绑定的行为很明确,推荐使用配置项的方法去绑定,也就是被动绑定。下面是将 demo 里面的绑定方式改成被动绑定。

<?php

namespace common\components;


use yii\base\Component;

class MyClass extends Component
{public function behaviors()
    {
        return ['test' => new BehaviorTest()
        ];
    }
}

此时可以将 demo 中 $myClass->attachBehavior('test', $myBehavior); 这句代码去掉,$myClass 也是同样可以调用类 BehaviorTest 里面的属性和方法

小结

这俩天查看了 Yii2.0 事件、行为的实现方式,觉得有很多相似的地方,都是通过 yii\base\Component 来实现的,通过打印的数据结构也可以看到,Component主要就是围绕 _events _eventWildcards _behaviors 这三个属性展开的,其中第二个属性是 事件的通配符模式,也可以归属到 事件中,那么这样 Component 的主要功能就是就是实现了 事件和行为。并且实现原理上也是相似的,都是往 Component 里面绑定事件和行为的handle,然后触发事件或者行为的时候,再去回调相应的handle。不过在解除的时候虽然都是删掉相应的handle,但是解除行为还需要解除在绑定行为的时候绑定的事件,这点不太一样。

正文完
 0