简述Yii2里的AOP思想
AOP是什么
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
AOP解决什么
我怎么看AOP
从上述定义中看,AOP是将非业务行为从业务代码中抽离出来,再利用某种技术手段将它们切入回业务逻辑中去,这样做就可以灵活的改变这些行为实现而不需要改动业务代码了。
提取公共代码我们都会做,但是怎么切回到业务代码中呢?这就是AOP实现决定的了。上述定义中,其实AOP技术一般通过预编译或者运行期动态代理的方法实现,因此我就谈谈运行期动态代理的实现原理,以便对AOP思想做一个了解。
AOP实现 – 运行期动态代理
一般AOP框架都是给业务代码提供一个前置和后置的切入调用点,以此方式实现非业务行为的切入。
因此,动态代理的方式也不难理解,这里假设你已经有一个业务函数,希望在函数前后各插入一个调用点以实现切面编程,那么不侵入的方案就是提供另外一个函数来封装它,大概这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 已有业务函数 function businessFunc() {} // 代理函数 function proxyFunc(doBeforeFunc, doAfterFunc) { // 前置切入点 call_user_func(doBeforeFunc); businessFunc(); // 后置切入点 call_user_func(doAfterFunc); } // 调用代理 proxyFunc(function(){echo "before";}, function(){echo "end";}); |
可见,虽然对businessFunc没有做修改,但是调用方式需要改为proxyFunc,因此运行期动态代理的方案其实并不方便,这也是为什么AOP通常都在预编译阶段实现,因为运行期实现在一定程度上会影响代码的本质面貌的,关于PHP编译阶段实现AOP的库可以看这个。
Yii2的折衷方案
各种编译阶段的AOP实现,一般支持前置,后置,包围 这3类切入方式,一般以单个函数作为包装的单元,这个可以看一下这个,这里不赘述。顺便一提,如果你对python的注解@了解的话,它就是这里提到的包围切入。
编译阶段AOP可以把埋点的过程隐藏起来,因此不需要对原有代码做任何修改。如果在运行期实现AOP,我们必须有所折衷,主要体现在:
- 显式的埋点,也就是通过在业务代码中,硬编码埋下AOP的调用点,优点是业务可以按需灵活埋点。
- 切面配置化生成,这样可以实现切面的灵活配置,仅需要修改配置而不需要修改业务代码。
在Yii2框架里,通过DI容器可以实现切面可配置,切面本身则是通过behavior行为模式和event事件机制彼此配合实现的,这样的实现符合AOP思想,可以解决切面类需求,又不依赖外部扩展,下面跟我来一探究竟。
event事件
切面的调用触发是投递一个event实现的,它记录了事件的类型,事件的内容,事件的触发者,下面是Event类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Event extends Object { /** * @var string the event name. This property is set by [[Component::trigger()]] and [[trigger()]]. * Event handlers may use this property to check what event it is handling. */ public $name; /** * @var object the sender of this event. If not set, this property will be * set as the object whose "trigger()" method is called. * This property may also be a `null` when this event is a * class-level event which is triggered in a static context. */ public $sender; /** * @var boolean whether the event is handled. Defaults to false. * When a handler sets this to be true, the event processing will stop and * ignore the rest of the uninvoked event handlers. */ public $handled = false; /** * @var mixed the data that is passed to [[Component::on()]] when attaching an event handler. * Note that this varies according to which event handler is currently executing. */ public $data; |
事件的触发者需要通过埋点的形式,将event投递出去,所有需要使用AOP的类都应该继承component类,它提供的trigger方法用来埋点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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); } |
$name是事件的类型,当然也可以显式的传入一个event对象,事件被依次投递给_events[$name]中的处理函数进行处理,一旦某个处理函数标记事件已处理完成,调用链将终止。同时需要注意,event的sender属性表明了事件来源,因此事件处理函数可以访问sender中的一些方法来通知sender某些处理结论。
那么,问题其实就是事件处理函数是如何配置到component对象中的呢?我之前说过,这是通过DI将配置文件中的信息注入到属性中实现的。component类本身符合DI注入规范,可以用于从配置中注入属性,这是因为它实现了__set方法供DI容器注入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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 event: attach event handler $this->on(trim(substr($name, 3)), $value); return; } elseif (strncmp($name, 'as ', 3) === 0) { // as behavior: attach behavior $name = trim(substr($name, 3)); $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value)); return; } else { // 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); } else { throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); } } |
如果注入的属性是on开头的,说明这个配置项是用于注册event的handler,__set方法会调用on函数将handler保存起来,以备后续trigger调用:
1 2 3 4 5 6 7 8 9 |
public function on($name, $handler, $data = null, $append = true) { $this->ensureBehaviors(); if ($append || empty($this->_events[$name])) { $this->_events[$name][] = [$handler, $data]; } else { array_unshift($this->_events[$name], [$handler, $data]); } } |
其中handler必须是callable的,也就是可以作为第一个参数传给call_user_func发起调用,最简单的就是一个全局函数的名字。
接下来,业务代码可以通过显式的调用trigger埋点引入切面,通过配置文件on规则注入的handler就会被自动的调用,AOP就此实现。
下面是Application类的run方法,它多次触发event实现切面编程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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; } } |
behavior行为
在yii2里,behavior提供了2种能力,一个是给现有类注入额外的方法和属性,一个是给现有类注入事件处理函数。
前者其实和php的trait语法功能类似,目的是解决PHP单继承的限制,后者提供了另外一种间接注入event handler到component的方式,今天只谈后者。
用户自定义的behavior类要继承自behavior基类,才能拥有上述2个能力:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
class Behavior extends Object { /** * @var Component the owner of this behavior */ public $owner; /** * Declares event handlers for the [[owner]]'s events. * * Child classes may override this method to declare what PHP callbacks should * be attached to the events of the [[owner]] component. * * The callbacks will be attached to the [[owner]]'s events when the behavior is * attached to the owner; and they will be detached from the events when * the behavior is detached from the component. * * The callbacks can be any of the following: * * - method in this behavior: `'handleClick'`, equivalent to `[$this, 'handleClick']` * - object method: `[$object, 'handleClick']` * - static method: `['Page', 'handleClick']` * - anonymous function: `function ($event) { ... }` * * The following is an example: * * ```php * [ * Model::EVENT_BEFORE_VALIDATE => 'myBeforeValidate', * Model::EVENT_AFTER_VALIDATE => 'myAfterValidate', * ] * ``` * * @return array events (array keys) and the corresponding event handler methods (array values). */ public function events() { return []; } /** * Attaches the behavior object to the component. * The default implementation will set the [[owner]] property * and attach event handlers as declared in [[events]]. * Make sure you call the parent implementation if you override this method. * @param Component $owner the component that this behavior is to be attached to. */ public function attach($owner) { $this->owner = $owner; foreach ($this->events() as $event => $handler) { $owner->on($event, is_string($handler) ? [$this, $handler] : $handler); } } /** * Detaches the behavior object from the component. * The default implementation will unset the [[owner]] property * and detach event handlers declared in [[events]]. * Make sure you call the parent implementation if you override this method. */ public function detach() { if ($this->owner) { foreach ($this->events() as $event => $handler) { $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler); } $this->owner = null; } } } |
$owner记录该行为对象注入到了哪个component对象,events函数返回要注入到component的事件处理函数,而attach和detach则用于将behavior对象注入到component中,它同时会将event handler通过component的on方法(此前讲过)注册进去。
回到此前component的__set方法,除了on规则外还有一个判断as规则的分支,该配置可以为component注入一个行为类,component会将该行为类对象通过了attachBehaviors方法保存起来以备后用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
private function attachBehaviorInternal($name, $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; } |
该方法首先调用behavior对象的attach方法将behavior关联到component,并将其随带的event handler注册到component中。
这样在后续访问component中不存在的属性或者调用不存在的方法时,__get和_call方法就可以到_behaviors数组中遍历每个行为对象,如果这些行为对象中存在该属性或方法,那么就可以保证正常访问。
__get方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public function __get($name) { $getter = 'get' . $name; if (method_exists($this, $getter)) { // read property, e.g. getName() return $this->$getter(); } else { // 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); } else { throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); } } |
__call方法:
1 2 3 4 5 6 7 8 9 10 |
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()"); } |
Yii2作者设计模式能力很强,我们应加强对该框架的深入学习。
利用event,基于AOP思想,实现了一套切面编程的能力。
利用behavior,实现了类似trait的代码复用能力。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~
