Swoft2 框架精华教程:面向切面编程(Aspect)

面向切面编程(AOP)框架是 Swoft 框架的关键组件之一。

面向方面编程需要将程序逻辑分解为称为所谓关注的不同部分。跨越应用程序多个点的功能称为横切关注点,这些横切关注点在概念上与应用程序的业务逻辑分离。有很多常见的很好的例子,例如日志记录,审计,声明式事务,安全性,缓存等等。

OOP中模块化的关键单元是类,而在AOP中,模块化的单元是方面。依赖注入可帮助您将应用程序对象与其他对象解耦,而AOP可帮助您将交叉关注与其影响的对象分离。AOP就像Perl,.NET,Java等编程语言中的触发器。Swoft AOP 组件提供拦截器来拦截应用程序。例如,执行方法时,可以在方法执行之前或之后添加额外的功能。

AOP术语

在我们开始使用AOP之前,让我们熟悉AOP的概念和术语。这些术语并非特定于 Swoft,而是与 AOP 相关。

Aspect

其实就是共有功能的实现。如日志切面、权限切面、事务切面等。在实际应用中通常是一个存放共有功能实现的普通PHP类(切面类),之所以能被AOP容器识别成切面,是在配置中指定的。

Join point

就是程序在运行过程中能够插入切面的地点。例如,方法调用、异常抛出或字段修改等,但Swoft只支持方法级的连接点。

Advice

是切面的具体实现。以 目标方法 (要被代理的方法)为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际应用中通常是指向切面类中的一个方法,具体属于哪类通知,同样是在配置中指定的。

Pointcut

用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。

Target object

目标被一个或多个方面提供建议。该对象将始终是代理对象,也称为建议对象。

切面声明

切入点声明

@PointBean(针对 Bean 中所有的方法)

  • include 定义包含的目标 bean 的集合(bean 名称,或者类全名)
  • exclude 定义排除的目标 bean 的集合(bean 名称,或者类全名)

示例:

通过 bean 的类名指定 bean 集合

php 复制
/**
 * Class PrintParamsAspect
 *
 * @Aspect()
 * @PointBean(
 *     include={CategoryData::class, TextData::class}
 * )
 */

通过 bean 的别名获取指定 bean 的集合

php 复制
/**
 * Class PrintParamsAspect
 *
 * @Aspect()
 * @PointBean(
 *     include={"beanName"}
 * )
 */

@PointAnnotation(针对使用了某个注解的类或方法)

  • include 定义需要切入的匹配集合,匹配的注解类名(bean 类名,或者别名)
  • exclude 定义需要排序的匹配集合,匹配的注解类名(bean 类名,或者别名)
php 复制
use Swoft\Http\Server\Annotation\Mapping\Controller;
/**
 * Class ViewAspect
 * @Aspect()
 * @PointAnnotation(include={Controller::class, 'aliasName'})
 */

@PointExecution(针对目标类的方法)

  • include 定义需要切入的匹配集合,匹配的类方法,支持正则表达式
  • exclude 定义需要排序的匹配集合,匹配的类方法,支持正则表达式

通过类方法指定不同的方法集合。

注意集合每个配置为字符串,不能用类似 Class::class 这种形式,因为注解进行解析的时候,无法识别 Class::method 这个为常量,会报错。

php 复制
/**
 * Class ViewAspect
 * @Aspect()
 * @PointExecution(include={"Renderer::renderContent"})
 */

使用正则表达式匹配方法,注意,类和方法都可以使用正则

根据双冒号,分为类和方法部分:

例如:Test::action 表示类路径中有 Test 的类,类其中有 public 方法 action 为目标切点

例如:Test::action.* 表示前缀类路径中有 Test,且方法前缀为 action 的方法需为目标切点

php 复制
/**
 * Class ViewAspect
 * @Aspect()
 * @PointExecution(include={"Renderer::renderContent"})
 */

示例

在指定切面方法中修改参数

around 方法因为包裹着真正的类执行代码,所以可以实际去修改其参数的具体值。其他通知方法只能读取相关数据,而无法对数据进行修改。

php 复制
class Renderer {
    
    /**
     * 在 $data 参数内自动注入一个参数
     * @param string      $content
     * @param array       $data
     * @param string|null $layout override default layout file
     *
     * @return string
     * @throws Throwable
     */
    public function renderContent(string $content, array $data = [], $layout = null): string
    {
        // Render layout
        if ($layout = $layout ?: $this->layout) {
            $main = $this->fetch($layout, $data);

            $content = str_replace($this->placeholder, $content, $main);
        }

        return $content;
    }
}

切面具体代码示例:

通过指定某个类的具体的方法(@PointExecution(include={"Renderer::renderContent"})),来将自定义的代码进行织入。

php 复制
/**
 * Class ViewAspect
 * @Aspect()
 * @PointExecution(include={"Renderer::renderContent"})
 */
class ViewAspect
{

    /**
     * @Around()
     * @param ProceedingJoinPoint $joinPoint
     * @return mixed
     * @throws \Throwable
     */
    function around(ProceedingJoinPoint $joinPoint)
    {
        $args = $joinPoint->getArgs();
        // @todo 将 seo 相关数据注入到页面.
        // @todo 将 js/css 相关数据注入到模板中.
        if (empty($args[1]['insert'])) {
            $args[1]['insert'] = '123';
        }
        return $joinPoint->proceed($args);
    }
}

Model 层方法执行后自动打印日志

通过指定具体的类(@PointBean(include={CategoryData::class, TextData::class})),将其中所有的方法执行前,打印出参数的日志。

php 复制
/**
 * Class PrintParamsAspect
 *
 * @Aspect()
 * @PointBean(
 *     include={CategoryData::class, TextData::class}
 * )
 */
class PrintParamsAspect
{
    /**
     * @Before()
     * @param JoinPoint $joinPoint
     * @return void
     */
    public function before(JoinPoint $joinPoint)
    {
        if (APP_DEBUG) {
            CLog::debug(
                'method: %s, params: %s',
                $joinPoint->getMethod(),
                var_export($joinPoint->getArgs(), true)
            );
        }
    }

}