- 属性(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
:<?php
namespace yii\base;
use Yii;
class Object extends BaseObject {
}
<?php
class 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 方法。
- 继承自
如下:
<?php
class Box extends yii\base\Object {
private $long
public 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 定义的构造函数中:
<?php
class BaseObject implements Configurable {
public function __construct($config = []) {
if (!empty($config)) {
Yii::configure($this, $config);
}
$this->init();
}
}
数组配置对象的能力在 Yii::configure()
中:
<?php
class 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
,他封装了与事件相关的有关数据,并提供一些功能函数作为辅助:<?php
namespace 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 时解除所有的 handler
if ($handler === null) {
unset($this->_events[$name], $this->_eventWildcards[$name]);
return true;
}
$removed = false;
if (isset($this->_events[$name])) {
// 遍历所有的 $handler
foreach ($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 调用 handler
call_user_func($handler[0], $event);
// 如果在某一 handler 中,将 $evnet->handled 设为 true,
// 就不再调用后续的 handler
if ($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 行为的要素
<?php
namespace yii\base;
class Behavior extends BaseObject {
// 指向行为本身所绑定的 Component 对象
public $owner;
private $_attachedEvents = [];
// Behavior 基类本身没用,主要是子类使用,重载这个函数返回一个数组表示行为所关联的事件
public function events() {}
// 绑定行为到 $owner
public 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()
方法,表示这个行为将对类的何种事件进行何种反馈即可:
<?php
namespace app\Components;
use yii\db\ActiveRecord;
use yii\base\Behavior;
class MyBehavior extends Behavior {
// 重载 events() 使得在事件触发时,调用行为中的一些方法
public function events() {
// 在 EVENT_BEFORE_VALIDATE 事件触发时,调用成员函数 beforeValidate
return [
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()
:
<?php
namespace 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 语句,要解除特性时,则要删除这个语句。换句话说,需要对类进行修改。
- 行为可以在在配置阶段进行绑定,而特性不行。
- 行为可以用于对事件进行反馈,而特性不行。
- 当出现命名冲突时,行为会自行排除冲突,自动使用先绑定的行为。而特性在发生冲突时,需要人为 干预,修改发生冲突的变量名、属性名、方法名。
倾向于使用特性的情况: