PHP `Aspect-Oriented Programming` (`AOP`) 与 `GoAop` 框架实践

PHP AOP & GoAop 框架实践:给你的代码加点魔法

各位观众老爷,大家好!我是今天的主讲人,江湖人称“代码老司机”。今天咱们不聊美女,不谈人生,就聊聊代码里那些让人眼前一亮的小技巧:AOP (Aspect-Oriented Programming),以及它的 PHP 实现 GoAop

别被“面向切面编程”这个名字吓到,其实它并没有想象中那么玄乎。简单来说,AOP 就是一种可以让你在不修改原有代码的基础上,给它“穿上马甲”的技术。这个马甲可以做很多事情,比如:

  • 记录日志: 每个函数执行前、后,自动记录日志,不用手动 echo 或者 var_dump 了。
  • 权限校验: 在用户访问某个页面前,先校验权限,没有权限就直接 die() 或者跳转。
  • 性能监控: 统计每个函数的执行时间,找出性能瓶颈。
  • 事务管理: 自动开启、提交、回滚事务,妈妈再也不用担心我忘记 commit 了!

等等等等,总之,AOP 的用途非常广泛,只要你想,就可以用它来做很多事情。

为什么要用 AOP?

可能有人会说:“老司机,你说的这些我用普通方法也能实现啊,干嘛要用 AOP 这么麻烦?”

问得好!这就是问题的关键。不用 AOP,你的代码可能会变成这样:

function login($username, $password) {
  // 记录日志
  Log::info("用户 {$username} 尝试登录");

  // 权限校验
  if (!check_permission('login')) {
    Log::error("用户 {$username} 没有登录权限");
    die("没有权限");
  }

  // 开启事务
  DB::beginTransaction();

  try {
    // 核心业务逻辑
    $user = User::findByUsername($username);
    if ($user && $user->verifyPassword($password)) {
      // 登录成功
      Session::set('user_id', $user->id);
      Log::info("用户 {$username} 登录成功");
    } else {
      // 登录失败
      Log::error("用户 {$username} 登录失败");
      throw new Exception("用户名或密码错误");
    }

    // 提交事务
    DB::commit();
  } catch (Exception $e) {
    // 回滚事务
    DB::rollBack();
    Log::error("登录失败: " . $e->getMessage());
    throw $e;
  }

  return true;
}

看到了吗?日志、权限、事务的代码和核心业务逻辑混在一起,就像一锅乱炖,不仅难以维护,而且代码重复率很高。如果每个函数都要加这些东西,那你的代码将会变得非常臃肿。

而 AOP 就可以把这些横切关注点(Cross-Cutting Concerns)从核心业务逻辑中分离出来,让你的代码更加干净、整洁、易于维护。

AOP 的核心概念

在深入 GoAop 之前,我们先来了解一下 AOP 的几个核心概念:

  • Aspect (切面): 一个模块化的横切关注点,比如日志、权限校验等。切面包含通知(Advice)和切点(Pointcut)。
  • Join Point (连接点): 程序执行过程中的一个点,比如函数调用、方法执行、异常抛出等。这些点都可以被切面“拦截”。
  • Advice (通知): 在 Join Point 处执行的代码,也就是“马甲”的具体内容。Advice 有多种类型:
    • Before (前置通知): 在 Join Point 之前执行。
    • After (后置通知): 在 Join Point 之后执行,无论 Join Point 是否成功执行。
    • AfterReturning (返回通知): 在 Join Point 成功执行后执行。
    • AfterThrowing (异常通知): 在 Join Point 抛出异常后执行。
    • Around (环绕通知): 包围 Join Point 的执行,可以完全控制 Join Point 的执行过程。
  • Pointcut (切点): 一个表达式,用于指定哪些 Join Point 应该被 Advice 拦截。比如,你可以指定只拦截 login() 函数的执行。
  • Weaving (织入): 将 Aspect 应用到目标对象的过程。织入可以在编译期、加载期或运行期进行。

用一张表来总结一下:

概念 解释 例子
Aspect 封装横切关注点的模块,包含通知和切点。 日志记录切面、权限校验切面
Join Point 程序执行过程中的一个点,可以被切面拦截。 函数调用、方法执行、异常抛出
Advice 在 Join Point 处执行的代码,有前置、后置、返回、异常、环绕等类型。 前置通知:记录日志;后置通知:清理资源
Pointcut 用于指定哪些 Join Point 应该被 Advice 拦截的表达式。 拦截所有以 get 开头的方法、拦截 login() 函数
Weaving 将 Aspect 应用到目标对象的过程,可以在编译期、加载期或运行期进行。 将日志记录切面应用到所有 Service 类的方法

GoAop 框架实战

好了,理论知识就到这里,接下来我们进入实战环节,看看如何使用 GoAop 框架在 PHP 中实现 AOP。

1. 安装 GoAop

首先,你需要安装 GoAop 框架。可以通过 Composer 来安装:

composer require goaop/goaop

2. 定义 Aspect

接下来,我们需要定义一个 Aspect,也就是“马甲”。Aspect 通常是一个 PHP 类,它包含 Advice 和 Pointcut。

例如,我们定义一个日志记录 Aspect:

<?php

namespace AppAspect;

use GoAopAspect;
use GoAopInterceptMethodInvocation;
use GoAopAround;
use PsrLogLoggerInterface;

class LogAspect implements Aspect
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * 记录方法执行时间
     *
     * @param MethodInvocation $invocation
     * @Around("execution(public **->*(*))") // 拦截所有 public 方法
     */
    public function aroundMethod(MethodInvocation $invocation)
    {
        $method = $invocation->getMethod();
        $className = $method->getDeclaringClass()->getName();
        $methodName = $method->getName();
        $args = $invocation->getArguments();

        $this->logger->info("调用方法:{$className}::{$methodName},参数:" . json_encode($args));

        $startTime = microtime(true);
        $result = $invocation->proceed(); // 执行原始方法
        $endTime = microtime(true);

        $executionTime = round($endTime - $startTime, 3);
        $this->logger->info("方法 {$className}::{$methodName} 执行完毕,耗时 {$executionTime} 秒");

        return $result;
    }
}

这个 Aspect 做了以下几件事情:

  • 实现了 GoAopAspect 接口,表示它是一个 Aspect 类。
  • 定义了一个 aroundMethod() 方法,使用了 @Around 注解,表示它是一个环绕通知。
  • @Around("execution(public **->*(*))") 是一个 Pointcut 表达式,表示拦截所有 public 方法的执行。 execution() 是一个函数, public 表示public方法, ** 是一个通配符,表示任意类名,-> 指向方法,* 也表示任意方法名, (*) 表示任意参数。
  • aroundMethod() 方法中,我们首先记录了方法名和参数,然后执行原始方法 ($invocation->proceed()),最后记录了方法的执行时间。

3. 配置 GoAop

接下来,我们需要配置 GoAop,告诉它哪些 Aspect 应该被应用。

在你的项目根目录下创建一个 aop.yml 文件,内容如下:

aspects:
  - AppAspectLogAspect

interceptors:
  - AppInterceptorDebugInterceptor

excludePaths:
  - /vendor/
  - /config/

这个配置文件指定了:

  • aspects:需要加载的 Aspect 类。
  • interceptors: 用于拦截某些类的定义或者实例化
  • excludePaths:不需要被 AOP 拦截的目录。

4. 注册 GoAop

在你的应用入口文件(比如 index.php)中,注册 GoAop:

<?php

require __DIR__ . '/vendor/autoload.php';

use GoCoreAspectKernel;
use GoCoreAspectContainer;
use PsrLogLoggerInterface;
use MonologLogger;
use MonologHandlerStreamHandler;

// 定义一个 Kernel 类
class MyApplicationAspectKernel extends AspectKernel
{
    /**
     * 配置容器所需依赖
     *
     * @param AspectContainer $container
     */
    protected function configureAop(AspectContainer $container)
    {
        // 创建一个 Monolog 实例
        $logger = new Logger('app');
        $logger->pushHandler(new StreamHandler(__DIR__ . '/logs/app.log', Logger::INFO));

        // 将 Logger 绑定到 LoggerInterface 接口
        $container->bind(LoggerInterface::class, $logger);

        // 注册你的 Aspect 类
        $container->registerAspect(new AppAspectLogAspect($logger));
    }
}

// 初始化 Kernel
$applicationKernel = MyApplicationAspectKernel::getInstance();
$applicationKernel->init([
    'debug' => true, // 启用调试模式
    'cacheDir' => __DIR__ . '/cache', // 设置缓存目录
    'includePaths' => [__DIR__] // 设置包含路径
]);

// 你的应用代码
class MyService {
    public function doSomething($name) {
        echo "Hello, " . $name . "!n";
    }
}

$service = new MyService();
$service->doSomething("World");

这段代码做了以下几件事情:

  • 引入了 Composer 自动加载器。
  • 创建了一个 MyApplicationAspectKernel 类,继承自 GoCoreAspectKernel
  • configureAop() 方法中,注册了 LogAspect 类,并注入了 Logger 实例。
  • 初始化了 AspectKernel,并设置了调试模式、缓存目录和包含路径。

5. 测试

现在,你可以运行你的代码,看看 AOP 是否生效了。如果一切正常,你应该能在日志文件中看到方法的执行时间和参数。

进阶用法

除了基本的环绕通知之外,GoAop 还提供了很多其他的功能,比如:

  • Pointcut 表达式: GoAop 使用 AspectJ 风格的 Pointcut 表达式,可以非常灵活地指定需要拦截的 Join Point。
  • 参数绑定: 你可以在 Advice 中获取 Join Point 的参数,并进行处理。
  • 异常处理: 你可以在 Advice 中捕获 Join Point 抛出的异常,并进行处理。
  • 自定义 Advice 类型: 你可以自定义 Advice 类型,实现更复杂的 AOP 功能。
  • 拦截器的使用: 你可以使用拦截器来拦截类的定义或者实例化,从而修改类的行为。

1. Pointcut 表达式

GoAop 使用 AspectJ 风格的 Pointcut 表达式,可以非常灵活地指定需要拦截的 Join Point。以下是一些常用的 Pointcut 表达式:

  • execution(public **->*(*)):拦截所有 public 方法的执行。
  • execution(protected **->*(*)):拦截所有 protected 方法的执行。
  • execution(private **->*(*)):拦截所有 private 方法的执行。
  • execution(* AppServiceUserService->getUserById(..)):拦截 AppServiceUserService 类的 getUserById() 方法的执行。 .. 表示任意参数。
  • within(AppController*):拦截 AppController 命名空间下的所有类的所有方法的执行。
  • annotation(AppAnnotationLoggable):拦截所有使用了 AppAnnotationLoggable 注解的方法的执行。

2. 参数绑定

你可以在 Advice 中获取 Join Point 的参数,并进行处理。

例如,我们可以修改 LogAspect 类,获取方法参数的值:

<?php

namespace AppAspect;

use GoAopAspect;
use GoAopInterceptMethodInvocation;
use GoAopAround;
use PsrLogLoggerInterface;

class LogAspect implements Aspect
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * 记录方法执行时间
     *
     * @param MethodInvocation $invocation
     * @Around("execution(public **->*(*))")
     */
    public function aroundMethod(MethodInvocation $invocation)
    {
        $method = $invocation->getMethod();
        $className = $method->getDeclaringClass()->getName();
        $methodName = $method->getName();
        $args = $invocation->getArguments();

        $this->logger->info("调用方法:{$className}::{$methodName},参数:" . json_encode($args));

        $startTime = microtime(true);
        $result = $invocation->proceed();
        $endTime = microtime(true);

        $executionTime = round($endTime - $startTime, 3);
        $this->logger->info("方法 {$className}::{$methodName} 执行完毕,耗时 {$executionTime} 秒,返回值:" . json_encode($result));

        return $result;
    }
}

在这个例子中,我们通过 $invocation->getArguments() 获取了方法的参数,并通过 $invocation->proceed() 方法的返回值获取了方法的返回值。

3. 异常处理

你可以在 Advice 中捕获 Join Point 抛出的异常,并进行处理。

例如,我们可以修改 LogAspect 类,捕获方法抛出的异常:

<?php

namespace AppAspect;

use GoAopAspect;
use GoAopInterceptMethodInvocation;
use GoAopAround;
use GoAopAfterThrowing;
use PsrLogLoggerInterface;

class LogAspect implements Aspect
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * 记录方法执行时间
     *
     * @param MethodInvocation $invocation
     * @Around("execution(public **->*(*))")
     */
    public function aroundMethod(MethodInvocation $invocation)
    {
        $method = $invocation->getMethod();
        $className = $method->getDeclaringClass()->getName();
        $methodName = $method->getName();
        $args = $invocation->getArguments();

        $this->logger->info("调用方法:{$className}::{$methodName},参数:" . json_encode($args));

        $startTime = microtime(true);
        try {
            $result = $invocation->proceed();
            $endTime = microtime(true);

            $executionTime = round($endTime - $startTime, 3);
            $this->logger->info("方法 {$className}::{$methodName} 执行完毕,耗时 {$executionTime} 秒,返回值:" . json_encode($result));

            return $result;
        } catch (Exception $e) {
            $this->logger->error("方法 {$className}::{$methodName} 抛出异常:" . $e->getMessage());
            throw $e; // 重新抛出异常
        }
    }

    /**
     * 记录异常
     *
     * @param MethodInvocation $invocation
     * @AfterThrowing("execution(public **->*(*))")
     */
    public function afterThrowingMethod(MethodInvocation $invocation, Exception $exception)
    {
        $method = $invocation->getMethod();
        $className = $method->getDeclaringClass()->getName();
        $methodName = $method->getName();

        $this->logger->error("方法 {$className}::{$methodName} 抛出异常:" . $exception->getMessage());
    }
}

在这个例子中,我们使用了 try...catch 语句捕获了方法抛出的异常,并通过 afterThrowingMethod 通知记录了异常信息。

4. 拦截器的使用

拦截器可以用于拦截类的定义或者实例化,从而修改类的行为。

例如,我们可以定义一个拦截器,用于修改类的属性:

<?php

namespace AppInterceptor;

use GoAopInterceptConstructorInterceptor;
use GoAopInterceptConstructorInvocation;

class DebugInterceptor implements ConstructorInterceptor
{
    public function construct(ConstructorInvocation $invocation)
    {
        $obj = $invocation->proceed();
        $className = get_class($obj);
        echo "创建了 {$className} 类的实例n";
        return $obj;
    }
}

然后,在 aop.yml 文件中配置拦截器:

aspects:
  - AppAspectLogAspect

interceptors:
  - AppInterceptorDebugInterceptor

excludePaths:
  - /vendor/
  - /config/

在这个例子中,我们定义了一个 DebugInterceptor 拦截器,用于在类实例化时输出一条调试信息。

注意事项

  • 性能影响: AOP 会增加代码的复杂度和运行时开销,因此需要谨慎使用,避免过度使用。
  • 调试困难: AOP 会改变代码的执行流程,可能会增加调试的难度。
  • 兼容性: GoAop 需要 PHP 7.1 或更高版本。

总结

AOP 是一种强大的编程技术,可以让你在不修改原有代码的基础上,给它“穿上马甲”,实现各种各样的功能。GoAop 是一个 PHP 的 AOP 框架,可以让你在 PHP 中轻松使用 AOP。

希望今天的讲座能对你有所帮助。记住,代码的世界充满了魔法,只要你敢于探索,就能发现更多的惊喜!

好了,今天的分享就到这里,谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注