• 属性(Property)
  • 事件(Event)
  • 行为(Behavior)

    1. 属性

    1.1 什么是属性

  • 从访问的形式看,属性与成员变量没有区别。比如:

    1. $obj->foo

    成员变量是就类的结构构成而言的概念,而属性是就类的功能逻辑而言的概念,两者紧密联系又相互区别。比如,我们说 People 类有一个成员变量 int $age ,表示年龄。那么这里年龄就是属性,$age 就是成员变量。

  • 成员变量和属性的区别与联系在于:

    • 成员变量是一个“内”概念,反映的是类的结构构成。属性是一个“外”概念,反映的是类的逻辑意义。
    • 成员变量没有读写权限控制,而属性可以指定为只读或只写,或可读可写。
    • 成员变量不对读出作任何后处理,不对写入作任何预处理,而属性则可以。
    • public 成员变量可以视为一个可读可写、没有任何预处理或后处理的属性。而 private 成员变量由于外部不可见,与属性“外”的特性不相符,所以不能视为属性。
    • 虽然大多数情况下,属性会由某个或某些成员变量来表示,但属性与成员变量没有必然的对应关系,比如与非门的 output 属性,就没有一个所谓的 $output 成员变量与之对应。

      1.2 属性的实现

      在 Yii 中,由 yii\base\Object 提供了对属性的支持,如果要使类支持属性,可以继承自 yii\base\Objectyii\base\Object 继承了 yii\base\BaseObject

      1. <?php
      2. namespace yii\base;
      3. use Yii;
      4. class Object extends BaseObject {
      5. }
      1. <?php
      2. class BaseObject implements Configurable {
      3. public function __get($name) {
      4. $getter = 'get' . $name;
      5. if (method_exists($this, $getter)) {
      6. return $this->$getter();
      7. } elseif (method_exists($this, 'set' . $name)) {
      8. throw new InvalidCallException('Getting write-only property: '
      9. . get_class($this) . '::' . $name);
      10. }
      11. throw new UnknownPropertyException('Getting unknown property: '
      12. . get_class($this) . '::' . $name);
      13. }
      14. public function __set($name, $value) {
      15. $setter = 'set' . $name;
      16. if (method_exists($this, $setter)) {
      17. $this->$setter($value);
      18. } elseif (method_exists($this, 'get' . $name)) {
      19. throw new InvalidCallException('Setting read-only property: '
      20. . get_class($this) . '::' . $name);
      21. } else {
      22. throw new UnknownPropertyException('Setting unknown property: '
      23. . get_class($this) . '::' . $name);
      24. }
      25. }
      26. }
  • 触发原理:通过php的魔术方法 __get()__set() 来实现。当读取和写入对象的一个不存在的成员变量时,get() set() 会被自动调用。也就是当调用$obj->foo,会自动调用$obj->getfoo();当赋值属性$obj->foo = $value,会自动调用$obj->setfoo($vaule).

  • 实现成员变量属性化步骤:
    • 继承自 yii/base/Object
    • 声明一个用于保存该属性的私有成员变量。
    • 提供 getter 或 setter 方法。

如下:

  1. <?php
  2. class Box extends yii\base\Object {
  3. private $long
  4. public function setlong($v) {
  5. $this->long = $v;
  6. }
  7. public function getlong() {
  8. return $this->long;
  9. }
  10. }

理论上long属性可以通过把权限private改为public,但不是好习惯:

  • 失去类的封装性。
  • 这样只能一致有读写权限。
  • 不能有预处理的功能,比如同时需要去除掉左右空格。
  • 如果希望属性没有写的权限,public要改回private,这时读的权限也没有了。而通过属性化处理,只需要注释掉setter方法。

    1.3 其他属性相关方法

  • __isset()
  • __unset()
  • hasProperty()
  • canGetProperty()
  • canSetProperty()

    1.4 Object和Component

  • yii\base\Component 继承了 yii\base\BaseObject 类,但是重载了 __get()__set() ,同时引入了事件、行为。

  • yii定位于一个基于组件的框架,yii几乎所有的核心类都派生于 yii\base\Component 。因此Component类具有三个重要特性:
    • 属性
    • 事件
    • 行为

这三个特性是丰富和拓展类功能,改变行为的重要切入点。

1.5 Object配置

Yii 提供了一个统一的配置对象的方式。这一方式贯穿整个 Yii。Application 对象的配置就是这种配置方式的体现:

  1. # 框架入口代码,加载配置
  2. $application = new yii\web\Application($config);

$config 本质上是一个各种配置项的数组。Yii 中就是统一使用数组的方式对对象进行配置,而实现这一切的关键就在 yii\base\Object 定义的构造函数中:

  1. <?php
  2. class BaseObject implements Configurable {
  3. public function __construct($config = []) {
  4. if (!empty($config)) {
  5. Yii::configure($this, $config);
  6. }
  7. $this->init();
  8. }
  9. }

数组配置对象的能力在 Yii::configure() 中:

  1. <?php
  2. class BaseYii{
  3. public static function configure($object, $properties)
  4. {
  5. foreach ($properties as $name => $value) {
  6. $object->$name = $value;
  7. }
  8. return $object;
  9. }
  10. }

配置的过程就是遍历 $config 配置数组,将数组的键作为属性名,以对应的数组元素的值对对象的属性赋值。因此,实现 Yii 这一统一的配置方式的要点有:

  • 继承自 yii\base\Object 。
  • 为对象属性提供 setter 方法,以正确处理配置过程。
  • 如果需要重载构造函数,将 $config 作为该构造函数的最后一个参数,并将该参数传递给父构造函数。
  • 重载的构造函数的最后,要调用父构造函数。
  • 如果重载了 yii\base\Object::init() 函数,注意一定要在重载函数的开头调用父类的 init() 。

如果配置数组的某个配置项,也是一个数组,就像 Application 里会引入诸多的Component 一样。如后面会看到的 $app->request 中的 request 属性就是一个对象。那么,在配置 $app 时,必然要配置到这个 reqeust 对象。既然 request 也是一个对象,那么他的配置要是按照 Yii 的规矩来,也就是用一个数组来配置它。
打印$config,可以看到(不一定与我一样):

  1. Array
  2. (
  3. [timeZone] => Asia/Shanghai
  4. [aliases] => Array()
  5. [vendorPath] => /data1/test/vendor
  6. [components] => Array()
  7. [id] => wxapp
  8. [controllerNamespace] => wxapp\controllers
  9. [basePath] => /data1/test/vendor
  10. [bootstrap] => Array()
  11. [defaultRoute] => site/index
  12. [modules] => Array()
  13. [params] => Array()
  14. )

最终会调用 Yii::configure() 函数。该函数不区分配置项是简单的数值还是数组,就直接使用 $object->$name = $value 完成属性的赋值。
为了使其正确配置,需要在其 setter 函数上做出正确的处理方式。Yii 应用 yii\web\Application 就是依靠定义专门的 setter 函数,实现自动处理配置项的。比如,配置项 components,在配置时调用了setComponents()的setter方法。(Yii 并未将该函数放在 yii\web\Application 里,而是放在父类 yii\di\ServiceLocator 里面)

  1. public function setComponents($components) {
  2. foreach ($components as $id => $component) {
  3. $this->set($id, $component);
  4. }
  5. }

这里有个成员函数,$this->set() ,他是服务定位器用来注册服务的方法。留待服务定位器(Service Locator) 部分再讲。

2. 事件

  • 使用事件,可以在特定的时点,触发执行预先设定的一段代码,事件既是代码解耦的一种方式,也是设计业务流程的一种模式。

    2.1 Yii中与事件相关的类

    Yii 中,事件是在 yii\base\Component 中引入的, yii\base\Object 不支持事件。所以,需要使用事件时,需要继承 yii\base\Component 。同时,Yii 中还有一个与事件紧密相关的 yii\base\Event ,他封装了与事件相关的有关数据,并提供一些功能函数作为辅助:

    1. <?php
    2. namespace yii\base;
    3. class Event extends BaseObject {
    4. public $name; // 事件名
    5. public $sender; // 事件发布者,通常是调用了 trigger() 的对象或类。
    6. public $handled = false; // 是否终止事件的后续处理
    7. public $data; // 事件相关数据
    8. private static $_events = []; // handler 数组
    9. private static $_eventWildcards = [];
    10. public static function on($class, $name, $handler, $data = null, $append = true) {
    11. // 用于绑定事件 handler
    12. }
    13. public static function off($class, $name, $handler = null) {
    14. // 用于取消事件 handler 绑定
    15. }
    16. public static function hasHandlers($class, $name) {
    17. // 用于判断是否有相应的 handler 与事件对应
    18. }
    19. public static function trigger($class, $name, $event = null) {
    20. // 用于触发事件
    21. }
    22. }

    2.2 事件handle

    所谓事件 handler 就是事件处理程序。对于一个事件 handler,可以是以下的形式提供:

    • 一个 PHP 全局函数的函数名。(如:trim)
    • 一个对象的方法,或一个类的静态方法。(如:[$person, ‘sayHello’],如果是类的静态方法,那应该是 [‘namespace\to\Person’, ‘sayHello’])
    • 匿名函数。如 function ($event) { … }

因为handle的调用时通过 call_user_func() 来实现的,因此,handle的形式,与call_user_func()的要求是一致的。

但无论是何种方式提供,一个事件 handler 必须具有以下形式:

  1. function (yii\base\Event $event) {
  2. }

2.3 事件的绑定与解除

2.3.1 事件的绑定

有了事件 handler,还要告诉 Yii,这个 handler 是负责处理哪种事件的。 yii\base\Component::on() 就是用来绑定的:

  1. <?php
  2. $person = new Person;
  3. // 使用 PHP 全局函数作为 handler 来进行绑定
  4. $person->on(Person::EVENT_GREET, 'person_say_hello');
  5. // 使用对象 $obj 的成员函数 say_hello 来进行绑定
  6. $person->on(Person::EVENT_GREET, [$obj, 'say_hello']);
  7. // 使用类 Greet 的静态成员函数 say_hello 进行绑定
  8. $person->on(Person::EVENT_GREET, ['app\helper\Greet', 'say_hello']);
  9. // 使用匿名函数
  10. $person->on(Person::EVENT_GREET, function ($event) {
  11. echo 'Hello';
  12. });

yii\base\Component 维护了一个变量 $_events[] 来记录 handler 数组,用来保存绑定的 handler:

  1. <?
  2. public function on($name, $handler, $data = null, $append = true) {
  3. $this->ensureBehaviors();
  4. // 绑定过程就是将 handler 写入 _event[]
  5. if ($append || empty($this->_events[$name])) {
  6. $this->_events[$name][] = [$handler, $data];
  7. } else {
  8. array_unshift($this->_events[$name], [$handler, $data]);
  9. }
  10. }

$_events[]数组的下标为事件名,数组元素是形如一系列 [$handler, $data] 的数组:
image.png

2.3.2 事件的解除

在解除时,就是使用 unset() 函数,处理 $_event[] 数组的相应元素。 yii\base\Component::off() 如下所示:

  1. <?
  2. public function off($name, $handler = null) {
  3. $this->ensureBehaviors();
  4. if (empty($this->_events[$name]) && empty($this->_eventWildcards[$name])) {
  5. return false;
  6. }
  7. // $handler === null 时解除所有的 handler
  8. if ($handler === null) {
  9. unset($this->_events[$name], $this->_eventWildcards[$name]);
  10. return true;
  11. }
  12. $removed = false;
  13. if (isset($this->_events[$name])) {
  14. // 遍历所有的 $handler
  15. foreach ($this->_events[$name] as $i => $event) {
  16. if ($event[0] === $handler) {
  17. unset($this->_events[$name][$i]);
  18. $removed = true;
  19. }
  20. }
  21. if ($removed) {
  22. $this->_events[$name] = array_values($this->_events[$name]);
  23. return $removed;
  24. }
  25. }
  26. }

2.3.3 事件的触发

事件的触发,需要调用 yii\base\Component::trigger()

  1. <?
  2. public function trigger($name, Event $event = null) {
  3. $this->ensureBehaviors();
  4. if (!empty($this->_events[$name])) {
  5. $eventHandlers = array_merge($eventHandlers, $this->_events[$name]);
  6. }
  7. if (!empty($eventHandlers)) {
  8. if ($event === null) {
  9. $event = new Event();
  10. }
  11. if ($event->sender === null) {
  12. $event->sender = $this;
  13. }
  14. $event->handled = false;
  15. $event->name = $name;
  16. // 遍历 handler 数组,并依次调用
  17. foreach ($eventHandlers as $handler) {
  18. $event->data = $handler[1];
  19. // 使用 PHP 的 call_user_func 调用 handler
  20. call_user_func($handler[0], $event);
  21. // 如果在某一 handler 中,将 $evnet->handled 设为 true,
  22. // 就不再调用后续的 handler
  23. if ($event->handled) {
  24. return;
  25. }
  26. }
  27. }
  28. // 触发类一级的事件
  29. Event::trigger($this, $name, $event);
  30. }

yii\base\Application 为 例, 他 定 义 了 两 个 事 件, EVENT_BEFORE_REQUESTEVENT_AFTER_REQUEST 分别在处理请求的前后触发:

  1. <?
  2. abstract class Application extends Module {
  3. // const 常量的形式,可以避免写错
  4. const EVENT_BEFORE_REQUEST = 'beforeRequest';
  5. const EVENT_AFTER_REQUEST = 'afterRequest';
  6. public function run() {
  7. try {
  8. $this->state = self::STATE_BEFORE_REQUEST;
  9. $this->trigger(self::EVENT_BEFORE_REQUEST);
  10. $this->state = self::STATE_HANDLING_REQUEST;
  11. $response = $this->handleRequest($this->getRequest());
  12. $this->state = self::STATE_AFTER_REQUEST;
  13. $this->trigger(self::EVENT_AFTER_REQUEST);
  14. $this->state = self::STATE_SENDING_RESPONSE;
  15. $response->send();
  16. $this->state = self::STATE_END;
  17. return $response->exitStatus;
  18. } catch (ExitException $e) {
  19. $this->end($e->statusCode, isset($response) ? $response : null);
  20. return $e->statusCode;
  21. }
  22. }
  23. }

2.3.4 多个事件 handler 的顺序

Yii 中是支持这种一对多的绑定的。Yii 是使用数组来保存 handler 的,并按顺序执行这些 handler。这意味着一般框架上预设的 handler 会先执行。要使后加上的事件 handler 先运行,只需在调用 yii\base\Component::on() 进行绑定时,将第四个参数设为 $append 设为 false 那么这个 handler 就会被放 在数组的最前面了,它就会被最先执行。

2.3.5 事件的级别

除了实例级别的事件外,还有类级别的事件。对于 Yii,由于 Application 是一个单例,所有的代码都可以访问这个单例。因此,有一个特殊级别的事件,全局事件。但是,本质上,他只是一个实例级别的事件。

1. 类级别事件

类级别的事件将会影响到所有该类的实例。与实例级别的事件不同,类级别事件的绑定需要使用 yii\base\Event::on()

  1. <?
  2. Event::on(
  3. Worker::className(), // 第一个参数表示事件发生的类
  4. Worker::EVENT_OFF_DUTY, // 第二个参数表示是什么事件
  5. function ($event) { // 对事件的处理
  6. echo $event->sender . ' 下班了';
  7. });

类级别事件的触发仍然是在 yii\base\Component::trigger() 中,Component::trigger()的最后一个语句:

  1. <?
  2. Event::trigger($this, $name, $event); // 触发类一级的事件

这个语句就触发了类级别的事件。可以通过 $event->handled = true ,来终止事件处理。
从 yii\base\Event::trigger() 的参数列表来看,比 yii\base\Component::trigger() 多了一个参数 $class表示这是哪个类的事件。因此,在保存 $_event[] 数组上,yii\base\Event 也比 yii\base\Component 要多一个维度:

  1. <?
  2. // Component 中的 $_event[] 数组
  3. $_event[$eventName][] = [$handler, $data];
  4. // Event 中的 $_event[] 数组
  5. $_event[$eventName][$calssName][] = [$handler, $data];

类级别事件的触发,应使用 yii\base\Event::trigger() 。这个函数不会触发实例级别的事件。值得注意
的是,$event->sender 在实例级别事件中,$event->sender 指向触发事件的实例,而在类级别事件中,指向
的是类名。在 yii\base\Event::trigger() 代码中,有:

  1. <?
  2. if (is_object($class)) {
  3. if ($event->sender === null) {
  4. $event->sender = $class;
  5. }
  6. $class = get_class($class);
  7. } else {
  8. $class = ltrim($class, '\\');
  9. }

2. 全局事件

接下来再讲讲全局级别事件。所谓的全局事件,本质上只是一个实例事件罢了。他只是利用了 Application 实例在整个应用的生命周期中全局可访问的特性,来实现这个全局事件的。当然,也可以将他绑定在任意全局可访问的的 Component 上。
全局事件一个最大优势在于:在任意需要的时候,都可以触发全局事件,也可以在任意必要的时候绑定, 或解除一个事件:

  1. <?
  2. Yii::$app->on('bar', function ($event) {
  3. echo get_class($event->sender); // 显示当前触发事件的对象的类名称
  4. });
  5. Yii::$app->trigger('bar', new Event(['sender' => $this]));

Yii::$app->on() 可以在任何地方调用,就可以完成事件的绑定。而 Yii::$app->trigger() 只要在绑定之后的任何时候调用就 OK 了。

3. 行为

使用行为(behavior)可以在不修改现有类的情况下,对类的功能进行扩充。通过将行为绑定到一个类,可以使类具有行为本身所定义的属性和方法,就好像类本来就有这些属性和方法一样。而且不需要写一个新的类去继承或包含现有类。
Yii 中的行为,其实是 yii\base\Behavior 类的实例,只要将一个 Behavior 实例绑定到任意的 yii\base\Component 实例上,这个 Component 就可以拥有该 Behavior 所定义的属性和方法了。而如果将行为与事件关联起来,可以玩的花样就更多了。
Behavior 只能与 Component 类绑定。所以一个类需要使用到行为,就果断地继承 yii\base\Component

3.1 使用行为

  1. <?
  2. // Step 1: 定义一个将绑定行为的类
  3. class MyClass extends yii\base\Component {
  4. // 空的
  5. }
  6. // Step 2: 定义一个行为类,他将绑定到 MyClass 上
  7. class MyBehavior extends yii\base\Behavior {
  8. // 行为的一个属性
  9. public $property1 = 'This is property in MyBehavior.';
  10. // 行为的一个方法
  11. public function method1() {
  12. return 'Method in MyBehavior is called.';
  13. }
  14. }
  15. $myClass = new MyClass();
  16. $myBehavior = new MyBehavior();
  17. // Step 3: 将行为绑定到类上
  18. $myClass->attachBehavior('myBehavior', $myBehavior);
  19. // Step 4: 访问行为中的属性和方法,就和访问类自身的属性和方法一样
  20. echo $myClass->property1;
  21. echo $myClass->method1();

3.2 行为的要素

  1. <?php
  2. namespace yii\base;
  3. class Behavior extends BaseObject {
  4. // 指向行为本身所绑定的 Component 对象
  5. public $owner;
  6. private $_attachedEvents = [];
  7. // Behavior 基类本身没用,主要是子类使用,重载这个函数返回一个数组表示行为所关联的事件
  8. public function events() {}
  9. // 绑定行为到 $owner
  10. public function attach($owner) {}
  11. // 解除绑定
  12. public function detach() {}
  13. }

3.2.1 行为的依附对象

yii\base\Behavior::$owner 指向的是 Behavior 实例本身所依附的对象。这是行为中引用所依附对象的唯一手段。通过这个 $owner ,行为才能访问所依附的 Component,才能将本身的方法作为事件 handler 绑定到Component 上。
$owner由 yii\base\Behavior::attach()赋值,在调用 yii\base\Componet::attachBehavior() 将行为与对象绑定时,Component 会自动地将 $this 作为参数,调用 yii\base\Behavior::attach()
由于行为本质是一个 PHP 类,其方法是类方法,就是成员函数。所以在行为的方法中,$this 引用的是行为本身,用 $this 来访问行为所依附的 Component 行不通。 正确的方法是通过 yii\base\Behavior::$owner 来访问 Component。


行为与事件结合后,可以在不对类作修改的情况下,补充类在事件触发后的各种不同反应。为此,只需要重载 yii\base\Behavior::events() 方法,表示这个行为将对类的何种事件进行何种反馈即可:

  1. <?php
  2. namespace app\Components;
  3. use yii\db\ActiveRecord;
  4. use yii\base\Behavior;
  5. class MyBehavior extends Behavior {
  6. // 重载 events() 使得在事件触发时,调用行为中的一些方法
  7. public function events() {
  8. // 在 EVENT_BEFORE_VALIDATE 事件触发时,调用成员函数 beforeValidate
  9. return [
  10. ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
  11. ];
  12. }
  13. // 注意 beforeValidate 是行为的成员函数,而不是绑定的类的成员函数。
  14. // 还要注意,这个函数的签名,要满足事件 handler 的要求。
  15. public function beforeValidate($event) {
  16. // ...
  17. }
  18. }

上面的代码中,events() 返回一个数组,表示所要做出响应的事件,上例中的事件是 ActiveRecord::EVENT_BEFORE_VALIDATE ,以数组的键来表示,而数组的值则表示响应事件 handler,上例中是 beforeValidate() ,事件 handler 可以是以下形式:

  • 字符串,表示行为类的方法,如上面的例就是这种情况。这个是与事件 handler 不同的,事件 handler

中使用字符串时,是表示 PHP 全局函数,而这里表示行为类内部的方法。

  • 一个对象或类的成员函数,以数组的形式,如 [$object, ’methodName’] 。这个与事件 handler 是一致

的。

  • 一个匿名函数。

对于事件响应函数的签名,要求与事件 handler 一样:

  1. function($event) {}

3.3 定义一个行为

定义一个行为,就是准备好要注入到现有类中去的属性和方法,这些属性和方法要写到一个 yii\base\Behavior 类中。所以,定义一个行为,主要工作是写一个 Behavior 的子类。

3.3.1 行为的绑定

行为的绑定通常是由 Component 来发起。有两种绑定方法,一种是静态的方法,另一种是动态的。静态的方法在实践中用得比较多一些。 因为一般情况下,在代码没跑起来之前,一个类应当具有何种行为,是确定的。动态绑定的方法主要是提供了更灵活的方式,但实际使用中并不多见。

1. 静态方法绑定行为

重载 yii\base\Component::behaviors()

  1. <?php
  2. namespace app\models;
  3. use yii\db\ActiveRecord;
  4. use app\Components\MyBehavior;
  5. class User extends ActiveRecord {
  6. public function behaviors() {
  7. return [
  8. // 匿名的行为,仅直接给出行为的类名称
  9. MyBehavior::className(),
  10. // 名为 myBehavior2 的行为,也是仅给出行为的类名称
  11. 'myBehavior2' => MyBehavior::className(),
  12. // 匿名行为,给出了 MyBehavior 类的配置数组
  13. [
  14. 'class' => MyBehavior::className(),
  15. 'prop1' => 'value1',
  16. 'prop3' => 'value3',
  17. ],
  18. // 名为 myBehavior4 的行为,也是给出了 MyBehavior 类的配置数组
  19. 'myBehavior4' => [
  20. 'class' => MyBehavior::className(),
  21. 'prop1' => 'value1',
  22. 'prop3' => 'value3',
  23. ]
  24. ];
  25. }
  26. }

或通过配置文件来绑定:

  1. <?php
  2. [
  3. 'as myBehavior2' => MyBehavior::className(),
  4. 'as myBehavior3' => [
  5. 'class' => MyBehavior::className(),
  6. 'prop1' => 'value1',
  7. 'prop3' => 'value3',
  8. ],
  9. ]

2. 动态方法绑定行为

动态绑定行为,需要调用 yii\base\Compoent::attachBehaviors()

  1. <?php
  2. $Component->attachBehaviors([
  3. 'myBehavior1' => new MyBehavior, // 这是一个命名行为
  4. MyBehavior::className(), // 这是一个匿名行为
  5. ]);

对于命名的行为,可以调用 yii\base\Component::getBehavior() 来取得这个绑定好的行为:

  1. <?
  2. $behavior = $Component->getBehavior('myBehavior2');

对于匿名的行为,则没有办法直接引用。但是,可以获取所有的绑定好的行为:

  1. <?
  2. $behavior = $Component->getBehavior();

3. 绑定的内部原理

实际的绑定过程:

  • yii\base\Component::behaviors()
  • yii\base\Component::ensureBehaviors()
  • yii\base\Component::attachBehaviorInternal()
  • yii\base\Behavior::attach()

    1. behaviors方法返回一个数组用于描述行为。
    2. ensureBehaviors个方法会在 Component 的诸多地方调用: __get() __set() __isset() __unset() __call() canGetProperty() hasMethod() hasEventHandlers() on() off() 等用到(只要涉及到类的属性、方法、事件这个函数都会被调用到),其作用是确保 behaviors() 中所描述的行为已经进行了绑定。

      1. <?
      2. public function ensureBehaviors() {
      3. if ($this->_behaviors === null) {
      4. $this->_behaviors = [];
      5. foreach ($this->behaviors() as $name => $behavior) {
      6. $this->attachBehaviorInternal($name, $behavior);
      7. }
      8. }
      9. }

      yii\base\Compoent 没有任何预先注入的行为,所以这个方法主要是给Component的子类用的。
      对于 yii\base\Component\attachBehaviorInternal() : ```php <? private function attachBehaviorInternal($name, $behavior) { // 若不是 Behavior 实例,则用Yii::createObject()创建 if (!($behavior instanceof Behavior)) { $behavior = Yii::createObject($behavior); }

      // 匿名行为 if (is_int($name)) { $behavior->attach($this); $this->_behaviors[] = $behavior;

    // 命名行为 } else {

    1. // 已经有一个同名的行为,要先解除,再将新的行为绑定上去。

    if (isset($this->_behaviors[$name])) {

    1. $this->_behaviors[$name]->detach();

    } $behavior->attach($this); $this->_behaviors[$name] = $behavior; }

    return $behavior; }

    1. 如上代码19行:以 $this 为参数调用了 yii\base\Behavior::attach():
    2. ```php
    3. <?
    4. public function attach($owner) {
    5. $this->owner = $owner;
    6. foreach ($this->events() as $event => $handler) {
    7. $this->_attachedEvents[$event] = $handler;
    8. $owner->on($event, is_string($handler) ? [$this, $handler] : $handler);
    9. }
    10. }
  • 设置好行为的 $owner ,使得行为可以访问、操作所依附的对象
  • 遍历行为中的 events() 返回的数组,将准备响应的事件,通过所依附类的 on() 绑定到类上

    4. 接触绑定

    解除行为只需调用 yii\base\Component::detachBehavior($name) 就 OK 了。对于匿名行为,这个方法就无从下手,但是可以解除所有绑定好的行为yii\base\Component::detachBehavior(),用到的是yii\base\Behavior::detach() :

    1. <?
    2. public function detach() {
    3. if ($this->owner) {
    4. foreach ($this->_attachedEvents as $event => $handler) {
    5. $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler);
    6. }
    7. $this->_attachedEvents = [];
    8. $this->owner = null;
    9. }
    10. }
  • 将 $owner 设置为 null,表示这 个行为没有依附到任何类上

  • 通过 Component 的 off() 将绑定到类上的事件 hanlder 解除下来

    5. 行为响应的事件实例

  • 要将行为与 Component 的事件关联起来,就要通过 yii\base\Behavior::events() 方法。


定义了在一个 ActiveRecord 对象的某些事件发 生时,自动对某些字段进行修改的行为:

  1. 在yii\base\Behavior的子类 yii\behaviors\AttributeBehavior::event() 中,代码如下:

    <?
    class AttributeBehavior extends Behavior {
         public function events() {
             return array_fill_keys(array_keys($this->attributes), 'evaluateAttributes');
         }
    }
    
  2. 在AttributeBehavior的子类 yii\behaviors\TimeStampBehavior::init() 中,有以下的代码:

    <?
    class TimestampBehavior extends AttributeBehavior {
     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 的初始化部分,数组的键值用于指定要响应的事件,这里是BaseActiveRecord::EVENT_BEFORE_INSERT 和 BaseActiveRecord::EVENT_BEFORE_UPDATE 。数组的值是一个事件 handler,如上面的 evaluateAttributes 。

  3. 若 TimeStampBehavior 与某个 ActiveRecord 绑定(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);
     }
    }
    

    因此事件 EVENT_BEFORE_INSERT 和 EVENT_BEFORE_UPDATE 就绑定到了ActiveRecord 上,当新建记录或更新记录时, TimeStampBehavior::evaluateAttributes 就会被触发。

    3.4 行为的属性和方法注入原理

    行为的属性和方法注入的原理分别是:对于属性而言,是通过 get() 和 set() 魔术方法来实现的。对于方法,是通过 __call() 方法。

    3.4.1 属性的注入

    以访问$Component->property1为例:

    <?
    public function __get($name) {
     $getter = 'get' . $name;
     if (method_exists($this, $getter)) {
         return $this->$getter();
     }
    
     // 与 yii\base\Object::__get() 的不同之处
     $this->ensureBehaviors();
     foreach ($this->_behaviors as $behavior) {
    
           // 属性在行为中须为 public。否则不可能通过下面的形式访问。
         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);
    }
    

    Component的 get() 与 Object 的 get() 的不同在于,检查到 getter 未定义后,Object 直接抛异常,而 Component 需要再检查是否存在注入的行为的属性。
    对于写入的 setter ,代码类似。

    3.4.2 方法的注入

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

    3.4.3 注入属性与方法的访问控制

    对于行为,一个属性可不可访问,主要看行为的 canGetProperty() 和 canSetProperty() 。而一个方法可不可调用,主要看行为的 hasMethod() 。 ```php <? 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); } ```

3.5 行为与集成和特性(Traits)的区别

3.5.1 行为与继承

相比较于使用继承的方式来扩充类功能,使用行为的方式,一是不必对现有类进行修改,二是 PHP 不 支持多继承,但是 Yii 可以绑定多个行为,从而达到类似多继承的效果。
从本质上来讲,行为只是一种设计模式,是解决问题的方法学。继承则是 PHP 作 为编程语言所提供的特性,根本不在一个层次上。

3.5.2 行为与特性

从实现效果看,行为与特性都达到把自身的 public 变量、 属性、方法注入到当前类中去的目的。各有所长。

倾向于使用行为的情况:

  • 行为从本质上讲,也是 PHP 的类,因此一个行为可以继承自另一个行为,从而实现代码的复用。而特性只是 PHP 的一种语法,效果上类似于把特性的代码导入到了类中从而实现代码的注入,特性是不支持继承的。
  • 行为可以动态地绑定、解除,而不必要对类进行修改。但是特性必须在类在使用 use 语句,要解除特性时,则要删除这个语句。换句话说,需要对类进行修改。
  • 行为可以在在配置阶段进行绑定,而特性不行。
  • 行为可以用于对事件进行反馈,而特性不行。
  • 当出现命名冲突时,行为会自行排除冲突,自动使用先绑定的行为。而特性在发生冲突时,需要人为 干预,修改发生冲突的变量名、属性名、方法名。

倾向于使用特性的情况:

  • 特性比行为在效率上要高一点,因为行为其实是类的实例,需要时间和空间进行分配。
  • 特性是 PHP 的语法,因此,IDE 的支持要好一些。