PHP 框架中的 AOP(切面编程)thinkphp5.0,yii2

本文介绍如何在thinkphp 5.0+中自定义标签拦截器(即自定义AOP行为)

1、定义一个Behavior处理器\application\index\behavior\Privilege.php

namespace app\index\behavior;


class Privilege
{
    public function run()
    {
        // 默认绑定的标签行为逻辑,如果该类中没有其它显示定义的标签处理方法,此会调用此方法
        echo '   default behavior goes here   ';
    }

    public function customListener1(){
        echo "<br/>".'custom_listener1'."<br/>";
    }

    public function customListener2(){
        echo "<br/>".'custom_listener2'."<br/>";
    }

    public function appEnd()
    {
        echo "<br/>appEnd...<br/>";
    }
}

2、定义Model业务层\application\index\model\User.php,并设置拦截器断点位置

namespace app\index\logic;


use think\Model;

class User extends Model
{

    function user_info(){
        $result  = array('success'=>true,'msg'=>'','data'=>array());

        echo '<br/>do some acton...<br/>';

        \think\Hook::listen('custom_listener1');

        echo '<br/>do another action...<br/>';

        \think\Hook::listen('custom_listener2');

        echo '<br/>all action done.<br/>';

        return $result;
    }
}

3、调用模型\application\index\controller\Index.php ,并激活拦截器(即绑定拦截标签与处理器的关系)

namespace app\index\controller;

use app\index\logic\User;
use think\controller\Rest;

class Index extends Rest
{
    function __construct()
    {
        parent::__construct();
        \think\Hook::add('custom_listener1','app\\index\\behavior\\Privilege');
        \think\Hook::add('custom_listener2','app\\index\\behavior\\Privilege');
    }

    function user_info(){
        /** @var \app\index\logic\User $userLogic */
        $userObj = new User();
        $user_info = $userObj->user_info();

        return 'ok';
    }
}

4、测试效果,访问http://localhost/index/index/user_info 

do some acton...

custom_listener1

do another action...

custom_listener2

all action done.

appEnd...
ok

注意到有 appEnd..输出是因为配置了框架自带的tags.php拦截规则,如

// 应用行为扩展定义文件
return [
    // 应用初始化
    'app_init'     => [],
    // 应用开始
    'app_begin'    => [],
    // 模块初始化
    'module_init'  => [],
    // 操作开始执行
    'action_begin' => [],
    // 视图内容过滤
    'view_filter'  => [],
    // 日志写入
    'log_write'    => [],
    // 应用结束
    'app_end'      => ['app\\index\\behavior\\Privilege'],
    'response_end'      => [],//no effected
];

Behavior几个要点 :

1、官方文档描述的几个拦截标签,如app_init,action_begin等,是系统内置支持的,也可以自定义任意的标签,并不局限于官方的几个;

2、要用好Behavior要注意三个步骤:a.处理器的定义(上面的Privilege.php类)、b.埋点标签(即\think\Hook::listen())、c.激活并绑定处理器与埋点标签(即\think\Hook::add()方法)

3、系统定义的拦截标签,激活的地方是有限制的,如app_init标签,如果你在某个controller里激活,是永远不会执行的,因为app_init方法执行时,controller类还没有执行到,所以在controller里定义是无效的,也就是只能在tags.php里配置了。

总结:官方文档关于Behavior描述有点不具体,需要花时间摸索才总结出来,在这记录一下希望对新手有所帮助。

---------------------------------------------------------------------------------------------------------------------------------------

                                 简述Yii2里的AOP思想

 

AOP是什么 
在软件业,AOP为Aspect Oriented Programming的缩写,意为: 面向切面编程 ,通过 预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。 
AOP解决什么 
将日志记录,性能统计,安全控制,事务处理, 异常处理 等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。 
我怎么看AOP 
从上述定义中看,AOP是将非业务行为从业务代码中抽离出来,再利用某种技术手段将它们切入回业务逻辑中去,这样做就可以灵活的改变这些行为实现而不需要改动业务代码了。 
提取公共代码我们都会做,但是怎么切回到业务代码中呢?这就是AOP实现决定的了。上述定义中,其实AOP技术一般通过预编译或者运行期动态代理的方法实现,因此我就谈谈运行期动态代理的实现原理,以便对AOP思想做一个了解。 
AOP实现 – 运行期动态代理 
一般AOP框架都是给业务代码提供一个前置和后置的切入调用点,以此方式实现非业务行为的切入。 
因此,动态代理的方式也不难理解,这里假设你已经有一个业务函数,希望在函数前后各插入一个调用点以实现切面编程,那么不侵入的方案就是提供另外一个函数来封装它,大概这样:

<?php
// 已有业务函数
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类:

<?php
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方法用来埋点:

<?php
  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容器注入:

<?php
    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->_behaviorsas $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调用:

<?php
    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实现切面编程:

<?php
  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个能力:

<?php
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方法保存起来以备后用:

<?php
    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数组中遍历每个行为对象,如果这些行为对象中存在该属性或方法,那么就可以保证正常访问。

<?php
//__get方法:
    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->_behaviorsas $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方法:
    public function __call($name, $params)
    {
        $this->ensureBehaviors();
        foreach ($this->_behaviorsas $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的代码复用能力。

总结一下,Aop 编程,不是很像 extends 继承的关系嘛 ,这样写有啥意义。其实某种程度上是把系统复杂化了,而且增加了看似不必要的操作。
但是做一些saas 系统,或者比较大的系统,aop编程,具有易读性和可扩展性,你啥时候开心了,就插入一个功能,啥时候不开心了,就拔了另外一个功能,而且比较不影响,后面的常规操作。
相对于 继承方式实现‘aop’实际中的例子就很明显,比如在某个接口你想有一些不要继承的类,但是硬生生的运行了,但是对该程序没什么软用。
 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值