Yii2 源码学习 Event

30 Oct 2016 Category: PHP

虽然PHP语言本身不能实现异步事件,但是并不代表事件在PHP中不是 一个非常有效的代码注入方式。TinkSNS中就有钩子,允许用户在不修 改主体业务代码的时候执行不同的操作。钩子对于一些活动,比如充 值送积分,充值打折扣。不同的活动,不一样,如果每次都要修改充 值业务的主体代码,不仅麻烦,而且容易产生bug。因此可以在充值成 功之后触发充值成功的事件,在事件处理中去处理相应的返现,优惠打 折操作。

看一下Event类的主体结构


class Event extends Object{

	public $name;
	public $sender;
	public $handled = false;
	private static $_events = [];
	public static function on($class, $name, $handler, $data = null, $append = true) {

	}

	public static function off($class, $name, $handler = null) {

	}

	public static function offAll() {}

	public static function hasHandlers($class, $name) {

	}

	public static function trigger($class, $name, $event = null) {

	}
}

整个类非常简单。事件绑定,事件解绑,解绑所有事件,是否绑定事件,触发事件五个函数,实现 事件处理机制。所有通过静态方法绑定的事件都存储在静态变量_events中。

1.事件绑定

	public static function on($class, $name, $handler, $data = null, $append = true) {
		$class = ltrim($class, '\\');
		if ($append || empty(self::$_events[$name][$class])) {
			self::$_events[$name][$class][] = [$handler, $data];
		} else {
			array_unshift(self::$_events[$name][$class], [$handler, $data]);
		}
	}

事件通过调用Event::on($class,$eventName,$handler,$data,$append)进行绑定。在event内部,相同类名, 相同事件名的事件handler和数据data放入数组中。根据$append的值添加到事件数组最末尾或最前面。

2.事件解绑


	public static function off($class, $name, $handler = null) {
		$class = ltrim($class, '\\');
		if (empty(self::$_events[$name][$class])) {
			return false;
		}
		if ($handler === null) {
			unset(self::$_events[$name][$class]);
			return true;
		} else {
			$removed = false;
			foreach (self::$_events[$name][$class] as $i => $event) {
				if ($event[0] === $handler) {
					unset(self::$_events[$name][$class][$i]);
					$removed = true;
				}
			}
			if ($removed) {
				self::$_events[$name][$class] = array_values(self::$_events[$name][$class]);
			}

			return $removed;
		}
	}

事件通过调用Event::off($class,$eventName,$handler)进行事件处理函数解绑。 通过handler的值解绑类的某个事件处理,删除在$_events中的数据。如果事件不存在,则返回false.如果handler 是空的,则解除这个类这个事件的所有事件处理函数。如果事件handler不为空,则删除指定的handler,并重新生 成事件handler数组(事件执行需要循环)。

3.事件触发


	public static function trigger($class, $name, $event = null) {
		if (empty(self::$_events[$name])) {
			return;
		}
		if ($event === null) {
			$event = new static;
		}
		$event->handled = false;
		$event->name = $name;

		if (is_object($class)) {
			if ($event->sender === null) {
				$event->sender = $class;
			}
			$class = get_class($class);
		} else {
			$class = ltrim($class, '\\');
		}

		$classes = array_merge(
			[$class],
			class_parents($class, true),
			class_implements($class, true)
		);

		foreach ($classes as $class) {
			if (!empty(self::$_events[$name][$class])) {
				foreach (self::$_events[$name][$class] as $handler) {
					$event->data = $handler[1];
					call_user_func($handler[0], $event);
					if ($event->handled) {
						return;
					}
				}
			}
		}
	}

在业务中,调用Event::trigger($class,$eventName,$event),触发事件,执行事件处理函数。 从_events数组中找到绑定改事件名的所有类,获取传入$class的类名,所有父类的类名,实现接口的 类名。判断这些类名是否在绑定改事件的类数组中,如果在则获取handler数组,调用call_user_func 执行。事件handler中,可以设置event->handled的值去停止执行后续事件处理函数。

从上面的触发机制可以看出来,子类触发事件,会触发父类的事件。因此在事件定义中,尽量把事 件安排在子类中。同时,如果需要传入在事件处理中传入参数,需要在事件绑定中传入data,不能通 过$event传入.

Yii中的事件分三个级别。一个是由Event绑定触发的类级别事件。还有就是由Commponent绑定的对象级别, 还有Yii::$app绑定触发的全局级别事件。

class Handler{
	public static afterPay($event){

	}
}
class Recharge{
	$chargeMoney;
	$user;

	public function pay(){
		//to-do pay for charge
		if($res)
		{
			Event::on(get_class($this),"AFTER_PAY",[Handler,afterPay],['money'=>$chargeMoney,'user'=>$user]);
			Event::trigger(get_class($this),"AFTER_PAY");
		}
	}
}

class Recharge extends Component{


	public function pay(){
		//to-do pay for charge
		if($res)
		{
			$this->on("AFTER_PAY",[Handler,afterPay],['money'=>$chargeMoney,'user'=>$user]);
			$this->trigger("AFTER_PAY");
		}
	}	
}

Component 事件触发函数

    public function trigger($name, Event $event = null)
    {
        $this->ensureBehaviors();
        if (!empty($this->_events[$name])) {
            if ($event === null) {
                $event = new Event;
            }
            if ($event->sender === null) {
                $event->sender = $this;
            }
            $event->handled = false;
            $event->name = $name;
            foreach ($this->_events[$name] 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);
    }

可以看出,Component事件触发跟Event的触发类似,重点在触发函数设置sender。类级别的sender是类名 对象级别的sender是对象。