kode/aop

基于 PHP 8.3+ 原生属性(Attribute)实现的轻量级、高性能、高扩展性 AOP(面向切面编程)组件

Maintainers

Package info

github.com/kodephp/aop

pkg:composer/kode/aop

Transparency log

Statistics

Installs: 74

Dependents: 0

Suggesters: 1

Stars: 1

Open Issues: 0

3.2.0 2026-08-11 02:49 UTC

This package is auto-updated.

Last update: 2026-08-11 02:51:05 UTC


README

PHP Version License

基于 PHP 8.3+ 原生属性(Attribute)实现的轻量级、高性能、高扩展性 AOP(面向切面编程)组件。

✨ 特性

  • 原生支持:基于 PHP 8.3+ 原生属性(Attribute)实现,IDE 友好
  • 轻量级:仅依赖 kode/attributes 包,无其他框架依赖
  • 门面 API:一行代码完成「注册切面 + 初始化 + 取代理」(Aop::boot() / Aop::proxy() / Aop::wrap()
  • 共享属性缓存:基于 kode/attributes 2.x,可注入共享缓存(如 RedisCache / APCu)让多进程复用反射元数据,Aop::setCache() 一行接入
  • 严格模式:通知属性实例化失败立即抛错(不再静默跳过),Aop::strict() 可切换
  • 五种通知:前置(Before)、后置(After)、环绕(Around)、返回后(AfterReturning)、异常(AfterThrowing)
  • 洋葱式 Around 链:支持同一方法上多个 Around 通知正确嵌套(修复 v2 仅优先级最高者生效的问题)
  • 丰富切入点execution / within / @annotation / @within / @target / method,支持 && || ! 逻辑运算、类名+ 子类型、参数类型签名
  • 命名空间代理:生成的代理类与目标类处于同一命名空间,彻底修复 v2 的 ParseError
  • 构造函数保留:代理类完全继承目标类构造函数,不会吞掉构造逻辑
  • final 类可代理:目标类为 final 时自动切换为组合式代理(实现接口 + 包装真实实例),不再要求目标类必须非 final;final 方法同样可被织入
  • 文件缓存:代理类可落盘为真实 PHP 文件并被 OPcache 缓存,且按切面集合指纹隔离,避免脏缓存
  • 类型安全:充分利用 PHP 8.3 的类型系统与 #[\Override] 属性
  • 优先级控制:通过 #[Priority] 注解控制通知执行顺序(After 系列遵循「先进后出」栈语义)

📦 安装

composer require kode/aop

🚀 快速开始

1. 创建切面类

<?php

use Kode\Aop\Attribute\Aspect;
use Kode\Aop\Attribute\Before;
use Kode\Aop\Attribute\After;
use Kode\Aop\Attribute\Around;
use Kode\Aop\Runtime\JoinPoint;
use Kode\Aop\Runtime\ProceedingJoinPoint;

#[Aspect]
class LoggingAspect
{
    #[Before("execution(* App\Service\UserService->createUser(..))")]
    public function logBefore(JoinPoint $joinPoint): void
    {
        $args = $joinPoint->getArguments();
        echo "准备创建用户: " . json_encode($args[0]) . "\n";
    }

    #[After("execution(* App\Service\UserService->createUser(..))")]
    public function logAfter(JoinPoint $joinPoint): void
    {
        echo "用户创建操作已完成\n";
    }
}

2. 配置 AOP 内核

<?php

use Kode\Aop\Runtime\AspectKernel;

$kernel = AspectKernel::getInstance();
$kernel->registerAspect(new LoggingAspect());
$kernel->init();

$userService = $kernel->getProxy(UserService::class);

3. 使用代理对象

<?php

$result = $userService->createUser([
    'name' => 'John Doe',
    'email' => 'john@example.com'
]);

Aop 门面(推荐用法)

Kode\Aop\Aop 是对内核单例的静态封装,一行即可完成「注册切面 + 初始化 + 取代理」:

use Kode\Aop\Aop;

// 方式一:直接传切面实例/类名
Aop::boot([LoggingAspect::class, TransactionAspect::class], __DIR__ . '/runtime/aop');

/** @var UserService $userService */
$userService = Aop::proxy(UserService::class);
$userService->getUser(1);

// 方式二:吃配置数组(结构见 config/aop.php)
Aop::bootFromConfig(require __DIR__ . '/config/aop.php');

// 把已有实例包装为代理(适合 DI 容器场景)
$proxied = Aop::wrap($alreadyCreatedService);

// 注入共享属性缓存(kode/attributes 2.x):多 worker / 多节点复用反射元数据
Aop::setCache(new \Kode\Attributes\Cache\RedisCache($redis));

// 严格模式默认开启;若需回退为宽容模式(属性实例化失败静默跳过)可关闭
Aop::strict(false);

门面还提供 Aop::advicesFor()(调试命中通知)、Aop::diagnostics()(运行期诊断)、Aop::reset()(测试隔离)等方法。

📖 详细文档

通知类型

Before(前置通知)

在目标方法执行前执行,可以修改方法参数或执行预处理逻辑。

#[Before("execution(* App\Service\*->*(..))")]
public function logBefore(JoinPoint $joinPoint): void
{
    $methodName = $joinPoint->getMethodName();
    $args = $joinPoint->getArguments();

    echo "方法 {$methodName} 即将执行\n";

    // 修改参数
    if (isset($args[0])) {
        $args[0] = trim($args[0]);
        $joinPoint->setArguments($args);
    }
}

After(后置通知)

在目标方法执行后执行(无论是否抛出异常),适用于资源清理、日志记录等场景。

#[After("execution(* App\Service\*->*(..))")]
public function logAfter(JoinPoint $joinPoint): void
{
    $result = $joinPoint->getResult();
    echo "方法执行完成,返回值: " . json_encode($result) . "\n";
}

Around(环绕通知)

环绕目标方法执行,可以完全控制方法的执行流程。

#[Around("execution(* App\Service\UserService->*(..))")]
public function transactional(ProceedingJoinPoint $joinPoint): mixed
{
    echo "开始事务\n";

    try {
        $result = $joinPoint->proceed();
        echo "提交事务\n";
        return $result;
    } catch (\Exception $e) {
        echo "回滚事务\n";
        throw $e;
    }
}

AfterReturning(返回后通知)

仅在目标方法正常返回后执行,可读取甚至替换返回值;与 #[After] 的区别是它不在异常时执行。

#[AfterReturning("execution(* App\Service\UserService->getUser(..))")]
public function cacheResult(JoinPointInterface $joinPoint): mixed
{
    $value = $joinPoint->getResult();
    // 写缓存……
    return $value; // 返回非 null 会覆盖原返回值;返回 null 保持原值
}

AfterThrowing(异常通知)

仅在目标方法抛出异常时执行,适用于异常上报、告警、审计。可通过 $throwable 限定只捕获特定异常类型;执行完毕后异常继续向上抛出(不吞异常)。

#[AfterThrowing("execution(* App\Service\*->*(..))", throwable: \RuntimeException::class)]
public function report(JoinPointInterface $joinPoint): void
{
    $e = $joinPoint->getException();
    error_log($e?->getMessage() ?? '');
}

优先级控制

使用 #[Priority] 注解控制切面执行顺序,数字越小优先级越高。

#[Aspect]
class PriorityAspect
{
    #[Before("execution(* App\Service\*->*(..))")]
    #[Priority(Priority::HIGHEST)]  // 最先执行
    public function first(JoinPoint $joinPoint): void
    {
        echo "第一个执行\n";
    }

    #[Before("execution(* App\Service\*->*(..))")]
    #[Priority(100)]
    public function second(JoinPoint $joinPoint): void
    {
        echo "第二个执行\n";
    }
}

切入点表达式

支持的切入点表达式语法:

表达式 说明 示例
execution(<修饰符> <返回> <类>-><方法>(<参数>)) 按方法签名匹配 execution(public * App\Service\UserService->createUser(..))
execution(* Class->method(..)) 执行方法 execution(* UserService->createUser(..))
execution(* Class->*(..)) 类的所有方法 execution(* UserService->*(..))
execution(* Namespace\*->*(..)) 命名空间下所有类的所有方法 execution(* App\Service\*->*(..))
within(Namespace\*) 命名空间下所有类 within(App\Controller\*)
within(Class+) 类及其子类 / 实现类 within(App\Service\BaseService+)
@annotation(Ann) 目标方法带有指定注解 @annotation(App\Attr\NoLog)
@within(Ann) / @target(Ann) 目标类带有指定注解 @within(App\Attr\Logged)
method(name) 仅按方法名匹配 method(createUser)
namedPointcut() 引用 #[Pointcut] 命名的切点 logAll()

execution 还支持:

  • 修饰符public / protected / private / static / final
  • 参数签名(..) 任意参数、() 无参、(int, string) 精确类型、(int, ..) 前缀类型匹配
  • 逻辑运算&&(与)、||(或)、!(非)、()(分组),关键字 and / or 等同
  • 子类型:类模式后缀 + 表示包含子类与实现类

示例:

// 匹配某命名空间下所有 save* 方法,但排除带 @NoLog 注解的方法
execution(* App\Service\*->save*(..)) && !@annotation(App\Attr\NoLog)

// 匹配基类及其全部子类的任意方法
within(App\Service\BaseService+)

通配符说明:

  • *:匹配任意数量的任意字符(类 / 方法名中均可使用)
  • ..:匹配任意参数列表
  • ?:匹配单个任意字符

JoinPoint API

JoinPoint 类提供了丰富的方法来获取方法调用的上下文信息:

$joinPoint->getClass();        // 获取目标类的反射对象
$joinPoint->getMethod();       // 获取目标方法的反射对象
$joinPoint->getThis();         // 获取目标对象实例
$joinPoint->getArguments();    // 获取方法参数
$joinPoint->setArguments([]);  // 设置方法参数
$joinPoint->getPointcut();     // 获取切入点表达式
$joinPoint->getResult();       // 获取返回值(After 通知)
$joinPoint->getMethodName();   // 获取方法名
$joinPoint->getClassName();    // 获取类名
$joinPoint->getArgument(0);    // 获取指定位置的参数

ProceedingJoinPoint API

ProceedingJoinPoint 继承自 JoinPoint,额外提供了控制原方法执行的能力:

$result = $joinPoint->proceed();                    // 使用原始参数执行
$result = $joinPoint->proceed(['newArg']);          // 使用新参数执行
$result = $joinPoint->proceedWithNamedParams([...]); // 使用命名参数执行
$closure = $joinPoint->getProceedClosure();         // 获取执行闭包

代理生成策略(继承 vs 组合)

框架按目标类是否 final 自动选择两种代理生成方式,对调用方完全透明

目标类 生成方式 代理关系 说明
final 继承式 class X__AopProxy extends X implements ProxyInterface 代理即目标类的子类,instanceof X 为真
final 组合式 class X__AopProxy implements <接口...>, ProxyInterface 代理实现目标接口并内部包装真实实例,instanceof X 为假

组合式代理(final 类)的特点:

  • 实现目标接口:代理 implements 目标类实现的全部接口,因此类型提示 接口 处仍可传代理;
  • 包装真实实例:内部持有 new X(...) 得到的真实对象,所有公开方法 / 接口方法转发给它执行;
  • 魔术委派:属性访问经 __get/__set/__isset/__unset,未显式声明的方法经 __call 兜底,均会经过 AOP 内核(命中通知则织入,否则直接透传);
  • final 方法可织入:因为只是调用被包装实例的方法而非覆盖它,final 方法同样可以被 Before/Around/AfterReturning 等通知介入;
  • wrap() 行为:组合式代理直接把被包装实例绑定到内部属性(不再逐个拷贝属性)。

提示:若要让 final 类被代理后仍能在「要求具体类」的位置使用,请让它实现一个接口,并以接口类型接收代理。

🏗️ 核心组件

src/
├── Aop.php                  # 门面:一行完成注册/初始化/取代理
├── Attribute/               # 原生注解定义
│   ├── Aspect.php           # 切面标记
│   ├── Before.php           # 方法前执行
│   ├── After.php            # 方法后执行(无论异常)
│   ├── Around.php           # 环绕执行(可控制流程)
│   ├── AfterReturning.php   # 返回后执行(可替换返回值)
│   ├── AfterThrowing.php    # 异常时执行(按异常类型过滤)
│   ├── Pointcut.php         # 命名切点
│   └── Priority.php         # 执行优先级
│
├── Pointcut/                # 切入点表达式
│   ├── PointcutParser.php   # 递归下降解析器(编译为匹配闭包)
│   └── MatchContext.php     # 匹配上下文(惰性反射)
│
├── Advice/                  # 通知编排
│   ├── Advice.php           # 单条通知值对象
│   ├── AdviceSet.php        # 某方法命中的通知集合
│   ├── AdviceRegistry.php   # 注册表 + 匹配结果缓存
│   ├── AdviceExecutor.php   # Before/Around/After... 时序编排
│   └── AdviceType.php       # 通知类型枚举
│
├── Proxy/                   # 代理生成
│   ├── ProxyGenerator.php   # 代理类源码生成器
│   └── ProxyFactory.php     # 命名/生成/缓存/实例化
│
├── Contract/                # 接口契约
│   ├── AspectInterface.php
│   ├── ProxyInterface.php   # 代理对象标记
│   ├── JoinPointInterface.php
│   ├── ProceedingJoinPointInterface.php
│   └── AspectKernelInterface.php
│
├── Runtime/                 # 运行时核心
│   ├── JoinPoint.php        # 封装调用上下文
│   ├── ProceedingJoinPoint.php # Around 场景专用(洋葱链)
│   └── AspectKernel.php     # 核心调度器
│
├── Reflection/              # 安全反射封装
│   ├── Reflector.php        # 安全获取类/方法/属性元数据
│   └── MetadataReader.php   # Attribute 元数据读取器
│
├── Exception/               # 自定义异常
│   └── AopException.php
│
└── Helper/                  # 工具函数
    └── Str.php              # 字符串匹配

🔧 框架集成

Laravel 集成

// app/Providers/AopServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Kode\Aop\Runtime\AspectKernel;

class AopServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(AspectKernel::class, function () {
            $kernel = AspectKernel::getInstance();
            $kernel->registerAspect(new \App\Aspects\LoggingAspect());
            $kernel->registerAspect(new \App\Aspects\TransactionAspect());
            $kernel->init();
            return $kernel;
        });
    }
}

Symfony 集成

// config/services.yaml
services:
    Kode\Aop\Runtime\AspectKernel:
        factory: ['@App\Aop\AspectKernelFactory', 'create']
        calls:
            - [init, []]

    App\Aop\AspectKernelFactory:
        class: App\Aop\AspectKernelFactory

Hyperf 集成

// config/autoload/dependencies.php
<?php

return [
    Kode\Aop\Runtime\AspectKernel::class => function () {
        $kernel = Kode\Aop\Runtime\AspectKernel::getInstance();
        $kernel->registerAspect(new \App\Aspect\LoggingAspect());
        $kernel->init();
        return $kernel;
    },
];

🧪 测试

运行测试:

composer test

运行代码覆盖率:

composer coverage

静态分析:

composer analyse

📋 系统要求

  • PHP >= 8.3
  • Composer >= 2.0
  • kode/attributes ^2.1

📄 许可证

Apache License 2.0

🤝 贡献

欢迎提交 Issue 和 Pull Request!

📮 联系方式