前言
今天这篇博客主要讲Yii2框架中的组件类component.php。这个组件类囊括了今天要讲的主题:属性、行为和事件。
首先在这里解释一下什么是属性,什么事行为,什么是事件。
属性
属性就是指类的成员变量,Yii2框架中的组件类component.php继承了对象基础类BaseObject.php。在对象基础类里面,运用php中的魔术方法__get()和__set(),实现对类属性的赋值和获取。
行为
行为和php的trait有点类似。使用行为必须继承组件类component.php(或行为类Behavior.php),它无须改变类继承关系,即可增强一个已有的组件类功能。 当行为附加到组件后,它将“注入”它的方法和属性到组件, 然后可以像访问组件内定义的方法和属性一样访问它们。
事件
事件和thinkPHP中钩子的概念差不多。事件可以将自定义代码“注入”到现有代码中的特定执行点。 附加自定义代码到某个事件,当这个事件被触发时,这些代码就会自动执行。
上源码
首先看下对象基础类BaseObject.php的源码,然后分析它主要的功能是什么。
BaseObject.php
<?php
class BaseObject implements Configurable
{
//这是我一个对象基础类,主要用于属性的设置和获取。
//当类的属性只有获取方法(getter),没有设置方法(setter)则认为此属性只读。
//生命周期:
//构造函数 -> 对象属性初始化 -> init函数
public static function className()
{
// 获取调用者完整类名
return get_called_class();
}
public function __construct($config = [])
{
// 存在配置,设置当前类默认属性。
if (!empty($config)) {
Yii::configure($this, $config);
}
$this->init();
}
public function init()
{
}
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);
}
}
public function __isset($name)
{
// 使用isset()判断对象属性是否存在时候调用。
$getter = 'get' . $name;
if (method_exists($this, $getter)) {
// 如果存在getter,则根据获取器返回值判断是否null。是null则false。否则ture;
return $this->$getter() !== null;
}
// 不存在getter直接false;
return false;
}
public function __unset($name)
{
// 使用unset()销毁对象属性时调用。
$setter = 'set' . $name;
if (method_exists($this, $setter)) { // 存在设置器,就设置为null.
$this->$setter(null);
} elseif (method_exists($this, 'get' . $name)) { // 不存在设置器,存在获取器,返回“销毁只读属性”错误。
throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '::' . $name);
}
// 否则不做处理。
}
public function __call($name, $params)
{
// 调用类的未知方法,直接抛出“调用未知方法”错误。
throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$name()");
}
public function hasProperty($name, $checkVars = true)
{
// 如果不存在获取器,并且类属性不存在。那么如果存在设置器,则返回真。
return $this->canGetProperty($name, $checkVars) || $this->canSetProperty($name, false);
}
public function canGetProperty($name, $checkVars = true)
{
// 是否存在获取器。如果第二参数为真,就是:是否存在类属性。
return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name);
// 上面的写法相当于
/*if (method_exists($this, 'get' . $name)) {
return method_exists($this, 'get' . $name);
} else {
if ($checkVars) {
return property_exists($this, $name);
} else {
return $checkVars;
}
}*/
}
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);
}
}
首先我们看到,这个类的构造方法,先通过配置的方式,设置了继承子类的初始属性。所以继承类不需要覆盖写当前类的构造方法,只需要覆盖init()这个方法即可。
其次,继承这个类的子类,可以添加设置器方法setter和获取器方法getter来对类的属性值进行特殊处理。整体代码也比较简单。这里不做过多赘述。
component.php
首先,yii框架的行为和事件的功能设计中,都用到了注册器模式。都是先把行为或者事件注册到组件类的全局树上($_events和$_behaviors)。然后再在程序里面,在需要的地方进行相应操作。我在博客中,把行为或者事件“注入”到全局树上的这个操作称为“绑定”,把行为或者事件“取消注入”全局树的操作称为“解绑”。这里将行为和事件分开来讲吧,我们先将实现原理,再来看源码。主要代码都在组件类component.php中。
事件
首先来讲事件,前面已经大概讲了一下事件的概念。使用事件,必须继承component.php组件类。整个事件的使用分为2~3个步骤。
1. 绑定事件到全局。
2. 调用trigger方法触发事件。
3.解绑事件。
事件的全局树上保存的是事件名称,以及事件的处理器(真正的执行程序)。事件的处理器可以是以下几种类型:
1. 类的静态方法
2. 对象的方法
3. 匿名函数
4. 全局函数
实现原理是这样的:
1. 绑定:将事件名称为键,事件处理器作为值(也是一个数组,可能为多个)作为值,保存到全局。
2. 触发:根据传入的事件名称,从全局树上获取对应的事件处理器。利用php函数call_user_func()。触发事件处理器的执行程序。
3. 解绑:正常情况下,解绑是没必要做的(事件类里面定义了事件是否触发的属性)。解绑也可以在事件触发之前调用。解绑就是把事件全局树上对应的事件销毁。(也可以销毁某一事件的某一具体的处理器)
行为
行为的概念前面也已经讲过了。这里具体讲一下使用和框架中的实现原理。
行为的使用主要是行为的绑定和解绑。绑定就是将行为类,绑定到组件中,那么这个组件就拥有了这个行为的全部方法和属性。解绑就是解除此行为对组件的绑定。
实现原理:
解绑的原理和事件是一样的。就是注销全局行为树种的一个值。这里主要讲绑定。
绑定的实现也非常简单。首先component.php继承了BaseObject.php,BaseObject.php里面针对普通类的属性的设置和获取做了优化,可以添加设置器或者获取器,来对属性做一些特殊处理。这里component.php覆盖写了父类BaseObject.php中的所有方法,进一步进行优化,__set()、__get()等魔术方法中添加了对行为的判断。例如__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);
}
我们可以看到,如果从当前组件中没有获取到属性的话,就会从当前组件绑定的行为中开始找,寻找行为对象中的属性,找到并返回。实现了对当前组件类的扩展。其他方法也是一样的。
最后附上component.php的源码解析:
<?php
namespace yii\base;
use Opis\Closure\ClosureStream;
use Yii;
use yii\helpers\StringHelper;
class Component extends BaseObject
{
private $_events = [];
private $_eventWildcards = [];
private $_behaviors;
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);
}
public function __set($name, $value)
{
// 魔术方法。
// 如果存在设置器, 就调用设置器
$setter = 'set' . $name;
if (method_exists($this, $setter)) {
// set property
$this->$setter($value);
return;
} elseif (strncmp($name, 'on ', 3) === 0) {
// "on "开头,表示是事件事件处理器,将事件处理器绑定到对应的时间名称
// on event: attach event handler
$this->on(trim(substr($name, 3)), $value);
return;
} elseif (strncmp($name, 'as ', 3) === 0) {
// "as "开头,表示是行为。将行为附加到行为对象全局树。
// as behavior: attach behavior
$name = trim(substr($name, 3));
$this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value));
return;
}
// 既不存在设置器,也不是事件和行为。那么可能是行为的属性。
// behavior property
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name)) {
$behavior->$name = $value;
return;
}
}
// 存在获取器,表示这个属性是只读的。
if (method_exists($this, 'get' . $name)) {
throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name);
}
throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name);
}
public function __isset($name)
{
$getter = 'get' . $name;
if (method_exists($this, $getter)) {
return $this->$getter() !== null;
}
// 区别于基类BaseObject的方法,新增一层行为的属性。
// behavior property
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) {
return $behavior->$name !== null;
}
}
return false;
}
public function __unset($name)
{
$setter = 'set' . $name;
if (method_exists($this, $setter)) {
$this->$setter(null);
return;
}
// behavior property
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name)) {
$behavior->$name = null;
return;
}
}
throw new InvalidCallException('Unsetting an unknown or read-only property: ' . get_class($this) . '::' . $name);
}
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()");
}
public function __clone()
{
// 被克隆的时候,清空克隆对象的行为和事件。
$this->_events = [];
$this->_eventWildcards = [];
$this->_behaviors = null;
}
public function hasProperty($name, $checkVars = true, $checkBehaviors = true)
{
return $this->canGetProperty($name, $checkVars, $checkBehaviors) || $this->canSetProperty($name, false, $checkBehaviors);
}
public function canGetProperty($name, $checkVars = true, $checkBehaviors = true)
{
if (method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name)) {
return true;
} elseif ($checkBehaviors) {
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name, $checkVars)) {
return true;
}
}
}
return false;
}
public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
{
if (method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name)) {
return true;
} elseif ($checkBehaviors) {
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name, $checkVars)) {
return true;
}
}
}
return false;
}
public function hasMethod($name, $checkBehaviors = true)
{
if (method_exists($this, $name)) {
return true;
} elseif ($checkBehaviors) {
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->hasMethod($name)) {
return true;
}
}
}
return false;
}
public function behaviors()
{
// 返回行为列表。默认都是空,如果需要添加,需要在子类覆盖
// 覆盖需注意:
// 返回的键是行为名称(也可以不设置键),返回的值是对应的扩展行为对象实例
return [];
}
public function hasEventHandlers($name)
{
// 又是他,这块是判断事件是否绑定了处理程序
$this->ensureBehaviors();
// 通配符模式,如果存在行为名称且存在事件处理,返回真。
foreach ($this->_eventWildcards as $wildcard => $handlers) {
if (!empty($handlers) && StringHelper::matchWildcard($wildcard, $name)) {
return true;
}
}
// 如果当前存在事件存在值,就返回真。否则在事件类里面判断。
return !empty($this->_events[$name]) || Event::hasHandlers($this, $name);
}
public function on($name, $handler, $data = null, $append = true)
{
// 将事件处理程序绑定到对应事件。时间处理程序支持以下几种方式:
// 1. 匿名函数
// 2. 函数的方法
// 3. 类的静态方法
// 4. 全局函数
$this->ensureBehaviors();
// 通配符模式
if (strpos($name, '*') !== false) {
if ($append || empty($this->_eventWildcards[$name])) { // 如果追加,或者当前事件名称的处理程序为空。就把当前处理程序给它加到最后一位。
$this->_eventWildcards[$name][] = [$handler, $data];
} else { // 如果不追加,那么就将事件处理程序加在第一位,优先处理。
array_unshift($this->_eventWildcards[$name], [$handler, $data]);
}
return;
}
// 普通模式
if ($append || empty($this->_events[$name])) {
$this->_events[$name][] = [$handler, $data];
} else {
array_unshift($this->_events[$name], [$handler, $data]);
}
}
public function off($name, $handler = null)
{
// 将事件处理程序移除对应事件
$this->ensureBehaviors();
// 如果当前事件不存在,返回false
if (empty($this->_events[$name]) && empty($this->_eventWildcards[$name])) {
return false;
}
// 如果处理器是null,直接清除当前事件所有处理器
if ($handler === null) {
unset($this->_events[$name], $this->_eventWildcards[$name]);
return true;
}
// 是否已经移除
$removed = false;
// plain event names
// 普通事件名称清除
if (isset($this->_events[$name])) { // 这个判断多余了吧?
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;
}
}
// wildcard event names
// 通配符事件名称处理
if (isset($this->_eventWildcards[$name])) { // 这个判断多余了吧?
foreach ($this->_eventWildcards[$name] as $i => $event) {
if ($event[0] === $handler) {
unset($this->_eventWildcards[$name][$i]);
$removed = true;
}
}
if ($removed) {
$this->_eventWildcards[$name] = array_values($this->_eventWildcards[$name]);
// remove empty wildcards to save future redundant regex checks:
if (empty($this->_eventWildcards[$name])) {
unset($this->_eventWildcards[$name]);
}
}
}
return $removed;
}
public function trigger($name, Event $event = null)
{
// 触发事件
$this->ensureBehaviors();
$eventHandlers = [];
foreach ($this->_eventWildcards as $wildcard => $handlers) {
// 如果当前事件名称,和通配符匹配了。就把通配符的处理器保存起来。
if (StringHelper::matchWildcard($wildcard, $name)) {
$eventHandlers = array_merge($eventHandlers, $handlers);
}
}
// 合并通配符匹配到的处理器,与当前事件的处理器。
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;
foreach ($eventHandlers as $handler) {
$event->data = $handler[1];
call_user_func($handler[0], $event);
// stop further handling if the event is handled
if ($event->handled) {
return;
}
}
}
// invoke class-level attached handlers
Event::trigger($this, $name, $event);
}
public function getBehavior($name)
{
$this->ensureBehaviors();
return isset($this->_behaviors[$name]) ? $this->_behaviors[$name] : null;
}
public function getBehaviors()
{
$this->ensureBehaviors();
return $this->_behaviors;
}
public function attachBehavior($name, $behavior)
{
$this->ensureBehaviors();
return $this->attachBehaviorInternal($name, $behavior);
}
public function attachBehaviors($behaviors)
{
$this->ensureBehaviors();
foreach ($behaviors as $name => $behavior) {
$this->attachBehaviorInternal($name, $behavior);
}
}
public function detachBehavior($name)
{
$this->ensureBehaviors();
if (isset($this->_behaviors[$name])) {
$behavior = $this->_behaviors[$name];
unset($this->_behaviors[$name]);
$behavior->detach();
return $behavior;
}
return null;
}
public function detachBehaviors()
{
$this->ensureBehaviors();
foreach ($this->_behaviors as $name => $behavior) {
$this->detachBehavior($name);
}
}
public function ensureBehaviors()
{
// 确保行为已经存在当前类的$this->_behaviors属性
// 只要是涉及到行为的操作的方法,都会调用一遍这个方法。这个其实可以在构造方法里面,只调用一次就可以了。不知道作者为啥要这样写。。。
if ($this->_behaviors === null) {
$this->_behaviors = [];
foreach ($this->behaviors() as $name => $behavior) {
$this->attachBehaviorInternal($name, $behavior);
}
}
}
private function attachBehaviorInternal($name, $behavior)
{
// 不属于行为类的对象,则生成一个实例($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;
}
}