AOP

    目录

    介绍

    首先,你不要看到 AOP 就感觉好难好复杂,看下去其实也就那样。而且在 imi 中你也不一定需要用到AOP,这是非必须的。

    AOP 的概念通过搜索引擎一定是看烦了,而且看了也没什么大卵用,不贴近实际。

    我先举个 AOP 实际应用的简单例子,比如在写一个方法的时候,可能要针对某个方法写前置和后置操作,传统写法如下:

    abstract class ParentClass
    {
        public function test()
        {
            $this->__beforeTest();
            // 做一些事情...
            echo 'Parent->test()', PHP_EOL;
            $this->__afterTest();
        }
    
        public abstract function __beforeTest();
    
        public abstract function __afterTest();
    }
    
    class Child extends ParentClass
    {
        public function __beforeTest()
        {
            echo 'Child->__beforeTest()', PHP_EOL;
        }
    
        public function __afterTest()
        {
            echo 'Child->__afterTest()', PHP_EOL;
        }
    }
    
    $child = new Child;
    $child->test();

    运行结果:

    Child->__beforeTest()
    Parent->test()
    Child->__afterTest()

    这种写法你需要事先定义好前置和后置方法,如果需要前后置的方法一多,写起来会非常繁琐。

    AOP 可以很好地解决这个问题,不仅可以在编写上不用事先定义这么多方法,还非常有助于解耦。

    AOP 名词

    切面 Aspect

    普通的类,你要切入的类。

    参数:

    名称描述默认值
    priority优先级,越大越先执行0

    切入点 PointCut

    普通类中的方法,你要切入的方法。

    参数:

    名称描述默认值
    type切入点类型,PointCutType::XXXPointCutType::METHOD
    allow允许的切入点[]
    deny不允许的切入点,即使包含中有的,也可以被排除[]
    priority优先级,越大越先执行,为 null 时使用 Aspect 设置null

    切入点类型(\Imi\Aop\PointCutType):

    名称描述
    METHOD方法,包括构造方法 __construct()1
    ANNOTATION带有注解的方法2
    CONSTRUCT构造方法3
    ANNOTATION_CONSTRUCT带有注解的类的构造方法4

    allow、deny 说明:

    注入METHOD时:支持格式类名::方法名,其中类名和方法名都支持代入*表示通配符

    注入CONSTRUCT时:支持格式类名,类名支持代入*表示通配符

    注入ANNOTATIONANNOTATION_CONSTRUCT时:只格式完整的类名

    连接点 Joinpoint

    在这个方法相关的什么时机触发通知,比如:调用的前置后置、抛出异常等。

    通知 Advice

    在连接点触发的通知,比如在前置操作触发,通知里写前置的具体实现。

    imi 支持的通知点有:

    Before

    前置操作,注意与Around区别。

    After

    后置操作

    Around

    环绕操作。先触发环绕操作,在前置操作前和后置操作后,都可以做一些事情。

    Before区别:Around先于Before执行,并可以完全不让原方法运行,可用于请求拦截等操作。

    AfterReturning

    在原方法返回后触发,可以修改返回值

    AfterThrowing

    在抛出异常后触发,允许设置allowdeny,设置允许和拒绝捕获的异常类

    通知执行顺序

    正常执行时

    Around → Before → $joinPoint->proceed()→ After → AfterReturning → Around

    有异常抛出时

    Around → Before → 抛出异常 → AfterThrowing

    使用方法

    使用注解注入方法

    监听池子的资源获取和释放:

    <?php
    namespace Test;
    
    use Imi\Aop\JoinPoint;
    use Imi\Aop\Annotation\After;
    use Imi\Aop\Annotation\Aspect;
    use Imi\Aop\Annotation\PointCut;
    
    #[Aspect]
    class Pool
    {
        /**
         * @param JoinPoint $a
         * @return void
         */
        #[
            PointCut(allow: [
                'Imi\*Pool*::getResource',
                'Imi\*Pool*::release',
            ]),
            After
        ]
        public function test(JoinPoint $joinPoint)
        {
            echo $joinPoint->getType() . ' ' . get_parent_class($joinPoint->getTarget()) . '::' . $joinPoint->getMethod() . '(): ' . $joinPoint->getTarget()->getFree() . '/' . $joinPoint->getTarget()->getCount() . PHP_EOL;
            var_dump('args:', $joinPoint->getArgs());
        }
    }

    运行效果:

    after Imi\Swoole\Redis\Pool\CoroutineRedisPool::getResource(): 0/1
    after Imi\Swoole\Redis\Pool\CoroutineRedisPool::release(): 1/1

    类名、方法名和命名空间没有要求。

    类注释中必须写Aspect表明是一个切面类

    方法中写PointCut表示指定切入点,支持通配符

    After代表在该方法调用后触发

    注入带有注解的方法

    可参考imi\src\Db\Aop\TransactionAop.php文件:

    #[Aspect]
    class TransactionAop
    {
        /**
         * 自动事务支持
         * @return mixed
         */
        #[
            PointCut(type: PointCutType::ANNOTATION, allow: [Transaction::class]),
            Around
        ]
        public function parseTransaction(AroundJoinPoint $joinPoint)
        {
        }
    }
    无论这个注解在方法上出现了几次,都只会触发一次注入处理

    配置注入

    实现代码

    namespace Test;
    
    use Imi\Aop\JoinPoint;
    
    class Test
    {
        /**
         * @param JoinPoint $a
         * @return void
         */
        public function test(JoinPoint $joinPoint)
        {
            echo $joinPoint->getType() . ' ' . get_parent_class($joinPoint->getTarget()) . '::' . $joinPoint->getMethod() . '(): ' . $joinPoint->getTarget()->getFree() . '/' . $joinPoint->getTarget()->getCount() . PHP_EOL;
        }
    }

    对类没有任何要求,方法只需要参数对即可。

    配置

    <?php
    return [
        // 类名
        \Test\Test::class    =>    [
            // 固定写法methods
            'methods'    =>    [
                // 方法名
                'test'    =>    [
                    // 指定切入点
                    'pointCut'    =>    [
                        'allow'    =>    [
                            "Imi\*Pool*::getResource",
                            "Imi\*Pool*::release",
                        ]
                    ],
                    'after'    =>    [
    
                    ]
                ]
            ]
        ],
    ];

    所有注入演示

    <?php
    namespace Test;
    
    use Imi\Aop\JoinPoint;
    use Imi\Aop\AroundJoinPoint;
    use Imi\Aop\Annotation\After;
    use Imi\Aop\Annotation\Around;
    use Imi\Aop\Annotation\Aspect;
    use Imi\Aop\Annotation\Before;
    use Imi\Aop\Annotation\PointCut;
    use Imi\Aop\AfterThrowingJoinPoint;
    use Imi\Aop\AfterReturningJoinPoint;
    use Imi\Aop\Annotation\AfterThrowing;
    use Imi\Aop\Annotation\AfterReturning;
    
    #[Aspect]
    class Test
    {
        /**
         * 前置操作
         * @param JoinPoint $a
         * @return void
         */
        #[
            PointCut(allow: ['ImiDemo\HttpDemo\MainServer\Model\Goods::getScore']),
            Before
        ]
        public function before(JoinPoint $joinPoint)
        {
            // 修改参数
            // $joinPoint->setArgs(/*参数数组*/);
            echo 'getScore()-before', PHP_EOL;
        }
    
        /**
         * 后置操作
         * @param JoinPoint $a
         * @return void
         */
        #[
            PointCut(allow: ['ImiDemo\HttpDemo\MainServer\Model\Goods::getScore']),
            After
        ]
        public function after(JoinPoint $joinPoint)
        {
            echo 'getScore()-after', PHP_EOL;
        }
    
        /**
         * 环绕
         * @return mixed
         */
        #[
            PointCut(allow: ['ImiDemo\HttpDemo\MainServer\Model\Goods::getScore1']),
            Around
        ]
        public function around(AroundJoinPoint $joinPoint)
        {
            var_dump('调用前');
            // 执行原方法,获取返回值
            $result = $joinPoint->proceed();
            // 执行原方法,获取返回值(方法返回值是引用返回时)
            // $result = $joinPoint->proceed(null, true);
            var_dump('调用后');
            return 'value'; // 无视原方法调用后的返回值,强制返回一个其它值
            return $result; // 返回原方法返回值
        }
    
        /**
         * 返回值
         * @param AfterReturningJoinPoint $joinPoint
         * @return void
         */
        #[
            PointCut(allow: ['ImiDemo\HttpDemo\MainServer\Model\Goods::getScore']),
            AfterReturning
        ]
        public function afterReturning(AfterReturningJoinPoint $joinPoint)
        {
            $joinPoint->setReturnValue('修改返回值');
        }
    
        /**
         * 异常捕获
         * @param AfterThrowingJoinPoint $joinPoint
         * @return void
         */
        #[
            PointCut(allow: ['ImiDemo\HttpDemo\MainServer\Model\Goods::getScore']),
            AfterThrowing
        ]
        public function afterThrowing(AfterThrowingJoinPoint $joinPoint)
        {
            // 异常不会被继续抛出,也不会记录日志
            $joinPoint->cancelThrow();
            var_dump('异常捕获:' . $joinPoint->getThrowable()->getMessage());
            // 如有需要,可以手动记录下日志:
            \Imi\Log\Log::error($joinPoint->getThrowable());
        }
    }
    

    属性注入

    如下代码例子,定义一个类,使用Inject注解来注释属性,在通过getBean()实例化时,会自动给被注释的属性赋值相应的实例对象。

    namespace Test;
    
    class TestClass
    {
        /**
         * 某Model对象
         */
        #[Inject(name: \XXX\Model\User::class)]
        protected $model;
    
        /**
         * 某Model对象,通过注释类型注入
         *
         * @var XXX\Model\User
         */
        #[Inject]
        protected $model2;
    
        /**
         * 某Model对象,类型声明注入
         */
        #[Inject]
        protected XXX\Model\User $model3;
    
        public function test()
        {
            var_dump($model->toArray());
        }
    }
    
    $testClass = App::getBean('Test\TestClass');
    $testClass->test();

    非 Bean 类使用属性注入

    imi 提供了一个 Imi\Bean\Traits\TAutoInject 来让非 Bean 类也能够使用属性注入。也就是直接new对象,也可以自动注入属性。

    无构造方法的类:

    namespace Test;
    
    use Imi\Aop\Annotation\Inject;
    
    class Test
    {
        use Imi\Bean\Traits\TAutoInject;
    
        #[Inject(name: 'XXX')]
        public $xxx;
    }
    
    $test = new Test;
    $test->xxx; // 会被自动注入,不用手动初始化

    有构造方法的类:

    namespace Test;
    
    use Imi\Aop\Annotation\Inject;
    
    class Test
    {
        use Imi\Bean\Traits\TAutoInject;
    
        #[Inject(name: 'XXX')]
        public $xxx;
    
        private $value;
    
        public function __construct()
        {
            $this->__autoInject(); // 手动调用 __autoInject() 方法
            $this->value = 123;
        }
    }
    
    $test = new Test;
    $test->xxx; // 会被自动注入,不用手动初始化

    方法参数注入

    /**
     * @return void
     */
    #[
        InjectArg(name: 'a', value: '123'),
        InjectArg(name: 'b', value: new Inject(name: \ImiDemo\HttpDemo\MainServer\Model\User::class))
    ]
    public function test($a, $b)
    {
        var_dump($a, $b);
    }

    可以直接注入值,也可以使用值注入注解。