- 属性(Property)
- 事件(Event)
-
1. 属性
1.1 什么是属性
从访问的形式看,属性与成员变量没有区别。比如:
$obj->foo
成员变量是就类的结构构成而言的概念,而属性是就类的功能逻辑而言的概念,两者紧密联系又相互区别。比如,我们说 People 类有一个成员变量 int $age ,表示年龄。那么这里年龄就是属性,$age 就是成员变量。
成员变量和属性的区别与联系在于:
- 成员变量是一个“内”概念,反映的是类的结构构成。属性是一个“外”概念,反映的是类的逻辑意义。
- 成员变量没有读写权限控制,而属性可以指定为只读或只写,或可读可写。
- 成员变量不对读出作任何后处理,不对写入作任何预处理,而属性则可以。
- public 成员变量可以视为一个可读可写、没有任何预处理或后处理的属性。而 private 成员变量由于外部不可见,与属性“外”的特性不相符,所以不能视为属性。
虽然大多数情况下,属性会由某个或某些成员变量来表示,但属性与成员变量没有必然的对应关系,比如与非门的 output 属性,就没有一个所谓的 $output 成员变量与之对应。
1.2 属性的实现
在 Yii 中,由
yii\base\Object提供了对属性的支持,如果要使类支持属性,可以继承自yii\base\Object。yii\base\Object继承了yii\base\BaseObject:<?phpnamespace yii\base;use Yii;class Object extends BaseObject {}
<?phpclass BaseObject implements Configurable {public function __get($name) {$getter = 'get' . $name;if (method_exists($this, $getter)) {return $this->$getter();} elseif (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);}public function __set($name, $value) {$setter = 'set' . $name;if (method_exists($this, $setter)) {$this->$setter($value);} elseif (method_exists($this, 'get' . $name)) {throw new InvalidCallException('Setting read-only property: '. get_class($this) . '::' . $name);} else {throw new UnknownPropertyException('Setting unknown property: '. get_class($this) . '::' . $name);}}}
触发原理:通过php的魔术方法
__get()和__set()来实现。当读取和写入对象的一个不存在的成员变量时,get() set() 会被自动调用。也就是当调用$obj->foo,会自动调用$obj->getfoo();当赋值属性$obj->foo = $value,会自动调用$obj->setfoo($vaule).- 实现成员变量属性化步骤:
- 继承自
yii/base/Object。 - 声明一个用于保存该属性的私有成员变量。
- 提供 getter 或 setter 方法。
- 继承自
如下:
<?phpclass Box extends yii\base\Object {private $longpublic function setlong($v) {$this->long = $v;}public function getlong() {return $this->long;}}
理论上long属性可以通过把权限private改为public,但不是好习惯:
- 失去类的封装性。
- 这样只能一致有读写权限。
- 不能有预处理的功能,比如同时需要去除掉左右空格。
- 如果希望属性没有写的权限,public要改回private,这时读的权限也没有了。而通过属性化处理,只需要注释掉setter方法。
1.3 其他属性相关方法
- __isset()
- __unset()
- hasProperty()
- canGetProperty()
-
1.4 Object和Component
yii\base\Component继承了yii\base\BaseObject类,但是重载了__get()和__set(),同时引入了事件、行为。- yii定位于一个基于组件的框架,yii几乎所有的核心类都派生于
yii\base\Component。因此Component类具有三个重要特性:- 属性
- 事件
- 行为
1.5 Object配置
Yii 提供了一个统一的配置对象的方式。这一方式贯穿整个 Yii。Application 对象的配置就是这种配置方式的体现:
# 框架入口代码,加载配置$application = new yii\web\Application($config);
$config 本质上是一个各种配置项的数组。Yii 中就是统一使用数组的方式对对象进行配置,而实现这一切的关键就在 yii\base\Object 定义的构造函数中:
<?phpclass BaseObject implements Configurable {public function __construct($config = []) {if (!empty($config)) {Yii::configure($this, $config);}$this->init();}}
数组配置对象的能力在 Yii::configure() 中:
<?phpclass BaseYii{public static function configure($object, $properties){foreach ($properties as $name => $value) {$object->$name = $value;}return $object;}}
配置的过程就是遍历 $config 配置数组,将数组的键作为属性名,以对应的数组元素的值对对象的属性赋值。因此,实现 Yii 这一统一的配置方式的要点有:
- 继承自 yii\base\Object 。
- 为对象属性提供 setter 方法,以正确处理配置过程。
- 如果需要重载构造函数,将 $config 作为该构造函数的最后一个参数,并将该参数传递给父构造函数。
- 重载的构造函数的最后,要调用父构造函数。
- 如果重载了 yii\base\Object::init() 函数,注意一定要在重载函数的开头调用父类的 init() 。
如果配置数组的某个配置项,也是一个数组,就像 Application 里会引入诸多的Component 一样。如后面会看到的 $app->request 中的 request 属性就是一个对象。那么,在配置 $app 时,必然要配置到这个 reqeust 对象。既然 request 也是一个对象,那么他的配置要是按照 Yii 的规矩来,也就是用一个数组来配置它。
打印$config,可以看到(不一定与我一样):
Array([timeZone] => Asia/Shanghai[aliases] => Array()[vendorPath] => /data1/test/vendor[components] => Array()[id] => wxapp[controllerNamespace] => wxapp\controllers[basePath] => /data1/test/vendor[bootstrap] => Array()[defaultRoute] => site/index[modules] => Array()[params] => Array())
最终会调用 Yii::configure() 函数。该函数不区分配置项是简单的数值还是数组,就直接使用 $object->$name = $value 完成属性的赋值。
为了使其正确配置,需要在其 setter 函数上做出正确的处理方式。Yii 应用 yii\web\Application 就是依靠定义专门的 setter 函数,实现自动处理配置项的。比如,配置项 components,在配置时调用了setComponents()的setter方法。(Yii 并未将该函数放在 yii\web\Application 里,而是放在父类 yii\di\ServiceLocator 里面)
public function setComponents($components) {foreach ($components as $id => $component) {$this->set($id, $component);}}
这里有个成员函数,$this->set() ,他是服务定位器用来注册服务的方法。留待服务定位器(Service Locator) 部分再讲。
2. 事件
使用事件,可以在特定的时点,触发执行预先设定的一段代码,事件既是代码解耦的一种方式,也是设计业务流程的一种模式。
2.1 Yii中与事件相关的类
Yii 中,事件是在
yii\base\Component中引入的,yii\base\Object不支持事件。所以,需要使用事件时,需要继承yii\base\Component。同时,Yii 中还有一个与事件紧密相关的yii\base\Event,他封装了与事件相关的有关数据,并提供一些功能函数作为辅助:<?phpnamespace yii\base;class Event extends BaseObject {public $name; // 事件名public $sender; // 事件发布者,通常是调用了 trigger() 的对象或类。public $handled = false; // 是否终止事件的后续处理public $data; // 事件相关数据private static $_events = []; // handler 数组private static $_eventWildcards = [];public static function on($class, $name, $handler, $data = null, $append = true) {// 用于绑定事件 handler}public static function off($class, $name, $handler = null) {// 用于取消事件 handler 绑定}public static function hasHandlers($class, $name) {// 用于判断是否有相应的 handler 与事件对应}public static function trigger($class, $name, $event = null) {// 用于触发事件}}
2.2 事件handle
所谓事件 handler 就是事件处理程序。对于一个事件 handler,可以是以下的形式提供:
- 一个 PHP 全局函数的函数名。(如:trim)
- 一个对象的方法,或一个类的静态方法。(如:[$person, ‘sayHello’],如果是类的静态方法,那应该是 [‘namespace\to\Person’, ‘sayHello’])
- 匿名函数。如 function ($event) { … }
因为handle的调用时通过 call_user_func() 来实现的,因此,handle的形式,与call_user_func()的要求是一致的。
但无论是何种方式提供,一个事件 handler 必须具有以下形式:
function (yii\base\Event $event) {}
2.3 事件的绑定与解除
2.3.1 事件的绑定
有了事件 handler,还要告诉 Yii,这个 handler 是负责处理哪种事件的。 yii\base\Component::on() 就是用来绑定的:
<?php$person = new Person;// 使用 PHP 全局函数作为 handler 来进行绑定$person->on(Person::EVENT_GREET, 'person_say_hello');// 使用对象 $obj 的成员函数 say_hello 来进行绑定$person->on(Person::EVENT_GREET, [$obj, 'say_hello']);// 使用类 Greet 的静态成员函数 say_hello 进行绑定$person->on(Person::EVENT_GREET, ['app\helper\Greet', 'say_hello']);// 使用匿名函数$person->on(Person::EVENT_GREET, function ($event) {echo 'Hello';});
yii\base\Component 维护了一个变量 $_events[] 来记录 handler 数组,用来保存绑定的 handler:
<?public function on($name, $handler, $data = null, $append = true) {$this->ensureBehaviors();// 绑定过程就是将 handler 写入 _event[]if ($append || empty($this->_events[$name])) {$this->_events[$name][] = [$handler, $data];} else {array_unshift($this->_events[$name], [$handler, $data]);}}
$_events[]数组的下标为事件名,数组元素是形如一系列 [$handler, $data] 的数组:
2.3.2 事件的解除
在解除时,就是使用 unset() 函数,处理 $_event[] 数组的相应元素。 yii\base\Component::off() 如下所示:
<?public function off($name, $handler = null) {$this->ensureBehaviors();if (empty($this->_events[$name]) && empty($this->_eventWildcards[$name])) {return false;}// $handler === null 时解除所有的 handlerif ($handler === null) {unset($this->_events[$name], $this->_eventWildcards[$name]);return true;}$removed = false;if (isset($this->_events[$name])) {// 遍历所有的 $handlerforeach ($this->_events[$name] as $i => $event) {if ($event[0] === $handler) {unset($this->_events[$name][$i]);$removed = true;}}if ($removed) {$this->_events[$name] = array_values($this->_events[$name]);return $removed;}}}
2.3.3 事件的触发
事件的触发,需要调用 yii\base\Component::trigger()
<?public function trigger($name, Event $event = null) {$this->ensureBehaviors();if (!empty($this->_events[$name])) {$eventHandlers = array_merge($eventHandlers, $this->_events[$name]);}if (!empty($eventHandlers)) {if ($event === null) {$event = new Event();}if ($event->sender === null) {$event->sender = $this;}$event->handled = false;$event->name = $name;// 遍历 handler 数组,并依次调用foreach ($eventHandlers as $handler) {$event->data = $handler[1];// 使用 PHP 的 call_user_func 调用 handlercall_user_func($handler[0], $event);// 如果在某一 handler 中,将 $evnet->handled 设为 true,// 就不再调用后续的 handlerif ($event->handled) {return;}}}// 触发类一级的事件Event::trigger($this, $name, $event);}
以 yii\base\Application 为 例, 他 定 义 了 两 个 事 件, EVENT_BEFORE_REQUEST 和 EVENT_AFTER_REQUEST 分别在处理请求的前后触发:
<?abstract class Application extends Module {// const 常量的形式,可以避免写错const EVENT_BEFORE_REQUEST = 'beforeRequest';const EVENT_AFTER_REQUEST = 'afterRequest';public function run() {try {$this->state = self::STATE_BEFORE_REQUEST;$this->trigger(self::EVENT_BEFORE_REQUEST);$this->state = self::STATE_HANDLING_REQUEST;$response = $this->handleRequest($this->getRequest());$this->state = self::STATE_AFTER_REQUEST;$this->trigger(self::EVENT_AFTER_REQUEST);$this->state = self::STATE_SENDING_RESPONSE;$response->send();$this->state = self::STATE_END;return $response->exitStatus;} catch (ExitException $e) {$this->end($e->statusCode, isset($response) ? $response : null);return $e->statusCode;}}}
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() :
<?Event::on(Worker::className(), // 第一个参数表示事件发生的类Worker::EVENT_OFF_DUTY, // 第二个参数表示是什么事件function ($event) { // 对事件的处理echo $event->sender . ' 下班了';});
类级别事件的触发仍然是在 yii\base\Component::trigger() 中,Component::trigger()的最后一个语句:
<?Event::trigger($this, $name, $event); // 触发类一级的事件
这个语句就触发了类级别的事件。可以通过 $event->handled = true ,来终止事件处理。
从 yii\base\Event::trigger() 的参数列表来看,比 yii\base\Component::trigger() 多了一个参数 $class表示这是哪个类的事件。因此,在保存 $_event[] 数组上,yii\base\Event 也比 yii\base\Component 要多一个维度:
<?// Component 中的 $_event[] 数组$_event[$eventName][] = [$handler, $data];// Event 中的 $_event[] 数组$_event[$eventName][$calssName][] = [$handler, $data];
类级别事件的触发,应使用 yii\base\Event::trigger() 。这个函数不会触发实例级别的事件。值得注意
的是,$event->sender 在实例级别事件中,$event->sender 指向触发事件的实例,而在类级别事件中,指向
的是类名。在 yii\base\Event::trigger() 代码中,有:
<?if (is_object($class)) {if ($event->sender === null) {$event->sender = $class;}$class = get_class($class);} else {$class = ltrim($class, '\\');}
2. 全局事件
接下来再讲讲全局级别事件。所谓的全局事件,本质上只是一个实例事件罢了。他只是利用了 Application 实例在整个应用的生命周期中全局可访问的特性,来实现这个全局事件的。当然,也可以将他绑定在任意全局可访问的的 Component 上。
全局事件一个最大优势在于:在任意需要的时候,都可以触发全局事件,也可以在任意必要的时候绑定, 或解除一个事件:
<?Yii::$app->on('bar', function ($event) {echo get_class($event->sender); // 显示当前触发事件的对象的类名称});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 使用行为
<?// Step 1: 定义一个将绑定行为的类class MyClass extends yii\base\Component {// 空的}// Step 2: 定义一个行为类,他将绑定到 MyClass 上class MyBehavior extends yii\base\Behavior {// 行为的一个属性public $property1 = 'This is property in MyBehavior.';// 行为的一个方法public function method1() {return 'Method in MyBehavior is called.';}}$myClass = new MyClass();$myBehavior = new MyBehavior();// Step 3: 将行为绑定到类上$myClass->attachBehavior('myBehavior', $myBehavior);// Step 4: 访问行为中的属性和方法,就和访问类自身的属性和方法一样echo $myClass->property1;echo $myClass->method1();
3.2 行为的要素
<?phpnamespace yii\base;class Behavior extends BaseObject {// 指向行为本身所绑定的 Component 对象public $owner;private $_attachedEvents = [];// Behavior 基类本身没用,主要是子类使用,重载这个函数返回一个数组表示行为所关联的事件public function events() {}// 绑定行为到 $ownerpublic function attach($owner) {}// 解除绑定public function detach() {}}
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() 方法,表示这个行为将对类的何种事件进行何种反馈即可:
<?phpnamespace app\Components;use yii\db\ActiveRecord;use yii\base\Behavior;class MyBehavior extends Behavior {// 重载 events() 使得在事件触发时,调用行为中的一些方法public function events() {// 在 EVENT_BEFORE_VALIDATE 事件触发时,调用成员函数 beforeValidatereturn [ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',];}// 注意 beforeValidate 是行为的成员函数,而不是绑定的类的成员函数。// 还要注意,这个函数的签名,要满足事件 handler 的要求。public function beforeValidate($event) {// ...}}
上面的代码中,events() 返回一个数组,表示所要做出响应的事件,上例中的事件是 ActiveRecord::EVENT_BEFORE_VALIDATE ,以数组的键来表示,而数组的值则表示响应事件 handler,上例中是 beforeValidate() ,事件 handler 可以是以下形式:
- 字符串,表示行为类的方法,如上面的例就是这种情况。这个是与事件 handler 不同的,事件 handler
中使用字符串时,是表示 PHP 全局函数,而这里表示行为类内部的方法。
- 一个对象或类的成员函数,以数组的形式,如 [$object, ’methodName’] 。这个与事件 handler 是一致
的。
- 一个匿名函数。
对于事件响应函数的签名,要求与事件 handler 一样:
function($event) {}
3.3 定义一个行为
定义一个行为,就是准备好要注入到现有类中去的属性和方法,这些属性和方法要写到一个 yii\base\Behavior 类中。所以,定义一个行为,主要工作是写一个 Behavior 的子类。
3.3.1 行为的绑定
行为的绑定通常是由 Component 来发起。有两种绑定方法,一种是静态的方法,另一种是动态的。静态的方法在实践中用得比较多一些。 因为一般情况下,在代码没跑起来之前,一个类应当具有何种行为,是确定的。动态绑定的方法主要是提供了更灵活的方式,但实际使用中并不多见。
1. 静态方法绑定行为
重载 yii\base\Component::behaviors() :
<?phpnamespace app\models;use yii\db\ActiveRecord;use app\Components\MyBehavior;class User extends ActiveRecord {public function behaviors() {return [// 匿名的行为,仅直接给出行为的类名称MyBehavior::className(),// 名为 myBehavior2 的行为,也是仅给出行为的类名称'myBehavior2' => MyBehavior::className(),// 匿名行为,给出了 MyBehavior 类的配置数组['class' => MyBehavior::className(),'prop1' => 'value1','prop3' => 'value3',],// 名为 myBehavior4 的行为,也是给出了 MyBehavior 类的配置数组'myBehavior4' => ['class' => MyBehavior::className(),'prop1' => 'value1','prop3' => 'value3',]];}}
或通过配置文件来绑定:
<?php['as myBehavior2' => MyBehavior::className(),'as myBehavior3' => ['class' => MyBehavior::className(),'prop1' => 'value1','prop3' => 'value3',],]
2. 动态方法绑定行为
动态绑定行为,需要调用 yii\base\Compoent::attachBehaviors() :
<?php$Component->attachBehaviors(['myBehavior1' => new MyBehavior, // 这是一个命名行为MyBehavior::className(), // 这是一个匿名行为]);
对于命名的行为,可以调用 yii\base\Component::getBehavior() 来取得这个绑定好的行为:
<?$behavior = $Component->getBehavior('myBehavior2');
对于匿名的行为,则没有办法直接引用。但是,可以获取所有的绑定好的行为:
<?$behavior = $Component->getBehavior();
3. 绑定的内部原理
实际的绑定过程:
- yii\base\Component::behaviors()
- yii\base\Component::ensureBehaviors()
- yii\base\Component::attachBehaviorInternal()
yii\base\Behavior::attach()
- behaviors方法返回一个数组用于描述行为。
ensureBehaviors个方法会在 Component 的诸多地方调用:
__get() __set() __isset() __unset() __call() canGetProperty() hasMethod() hasEventHandlers() on() off()等用到(只要涉及到类的属性、方法、事件这个函数都会被调用到),其作用是确保 behaviors() 中所描述的行为已经进行了绑定。<?public function ensureBehaviors() {if ($this->_behaviors === null) {$this->_behaviors = [];foreach ($this->behaviors() as $name => $behavior) {$this->attachBehaviorInternal($name, $behavior);}}}
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 {
// 已经有一个同名的行为,要先解除,再将新的行为绑定上去。
if (isset($this->_behaviors[$name])) {
$this->_behaviors[$name]->detach();
} $behavior->attach($this); $this->_behaviors[$name] = $behavior; }
return $behavior; }
如上代码19行:以 $this 为参数调用了 yii\base\Behavior::attach():```php<?public function attach($owner) {$this->owner = $owner;foreach ($this->events() as $event => $handler) {$this->_attachedEvents[$event] = $handler;$owner->on($event, is_string($handler) ? [$this, $handler] : $handler);}}
- 设置好行为的 $owner ,使得行为可以访问、操作所依附的对象
遍历行为中的 events() 返回的数组,将准备响应的事件,通过所依附类的 on() 绑定到类上
4. 接触绑定
解除行为只需调用
yii\base\Component::detachBehavior($name)就 OK 了。对于匿名行为,这个方法就无从下手,但是可以解除所有绑定好的行为yii\base\Component::detachBehavior(),用到的是yii\base\Behavior::detach() :<?public function detach() {if ($this->owner) {foreach ($this->_attachedEvents as $event => $handler) {$this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler);}$this->_attachedEvents = [];$this->owner = null;}}
将 $owner 设置为 null,表示这 个行为没有依附到任何类上
通过 Component 的 off() 将绑定到类上的事件 hanlder 解除下来
5. 行为响应的事件实例
要将行为与 Component 的事件关联起来,就要通过 yii\base\Behavior::events() 方法。
定义了在一个 ActiveRecord 对象的某些事件发 生时,自动对某些字段进行修改的行为:
在yii\base\Behavior的子类
yii\behaviors\AttributeBehavior::event()中,代码如下:<? class AttributeBehavior extends Behavior { public function events() { return array_fill_keys(array_keys($this->attributes), 'evaluateAttributes'); } }在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 。
若 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 语句,要解除特性时,则要删除这个语句。换句话说,需要对类进行修改。
- 行为可以在在配置阶段进行绑定,而特性不行。
- 行为可以用于对事件进行反馈,而特性不行。
- 当出现命名冲突时,行为会自行排除冲突,自动使用先绑定的行为。而特性在发生冲突时,需要人为 干预,修改发生冲突的变量名、属性名、方法名。
倾向于使用特性的情况:
