装饰器
OxPHP 装饰器使用 PHP 8 属性拦截 PHP 函数和方法调用。为任意函数或方法添加一个属性,OxPHP 便会在每次调用前后调用你的装饰器的 before() 和 after() 方法,而无需改动原始代码。
工作原理
- 定义一个装饰器类,实现
OxPHP\Decorator\AttributeInterface接口并用#[Attribute]标注。 - 在引导阶段用
oxphp_register_decorator(ClassName::class)注册一次。 - 将该属性应用到任意函数、方法或类上。
- 首次调用被装饰的函数时,OxPHP 检测到该属性并安装拦截钩子。
- 之后每次调用时,
before()在函数执行前运行,after()在函数返回后运行。
编写装饰器
一个装饰器类需要两样东西:#[Attribute] 标注以及对 AttributeInterface 的实现。
<?php
use OxPHP\Decorator\AttributeInterface;
use OxPHP\Decorator\Context;
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
class Timer implements AttributeInterface
{
private float $start;
public function __construct(
public readonly string $label = '',
) {}
public function before(Context $ctx): void
{
$this->start = hrtime(true);
}
public function after(Context $ctx): void
{
$elapsed = (hrtime(true) - $this->start) / 1e6;
error_log(sprintf('[Timer] %s: %.2fms', $this->label ?: $ctx->target, $elapsed));
}
}在引导阶段、在任何被装饰的函数被调用之前注册该装饰器:
<?php
require __DIR__ . '/../vendor/autoload.php';
oxphp_register_decorator(Timer::class);将它应用到函数和方法上:
<?php
#[Timer]
function processOrder(int $orderId): void
{
// Timer::before() runs before this
// Timer::after() runs after this
}
#[Timer(label: 'db-query')]
function fetchUser(int $id): array
{
return $db->query('SELECT * FROM users WHERE id = ?', [$id]);
}
class PaymentService
{
#[Timer(label: 'payment')]
public function charge(float $amount): bool
{
// ...
}
}类级别装饰器
将属性应用到一个类上,即可装饰它的所有方法:
<?php
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Audit implements AttributeInterface
{
public function before(Context $ctx): void
{
error_log("Calling {$ctx->target}");
}
public function after(Context $ctx): void
{
$status = $ctx->hasResult() ? 'ok' : 'error';
error_log("Finished {$ctx->target}: {$status}");
}
}
// Register
oxphp_register_decorator(Audit::class);
// Every method in this class is now audited
#[Audit]
class OrderService
{
public function create(array $data): int { /* ... */ }
public function cancel(int $id): void { /* ... */ }
}Context 对象
before() 和 after() 都会接收一个 OxPHP\Decorator\Context 对象,其中包含被装饰调用的相关信息。
属性
| 属性 | 类型 | 描述 |
|---|---|---|
$target |
string |
完整目标名称:App\Service::method 或 my_function |
$class |
string |
类名,对于独立函数为 "" |
$method |
string |
方法名,对于独立函数为 "" |
$function |
string |
独立函数的函数名,对于方法为 "" |
$objectId |
int |
被调用对象的 spl_object_id(),对于函数和静态方法为 0 |
$requestId |
string |
当前请求 ID |
$traceId |
string |
当前 W3C 追踪 ID。未启用分布式追踪时为空字符串 |
方法
| 方法 | 可用于 | 描述 |
|---|---|---|
getParams(): array |
before() 和 after() |
传递给被装饰函数的参数 |
getResult(): mixed |
仅 after() |
被装饰函数的返回值。在 before() 中或发生异常后返回 null |
hasResult(): bool |
仅 after() |
若函数成功返回且未抛出异常则为 true |
检查参数
getParams() 以数字索引数组的形式返回参数:
<?php
#[Attribute(Attribute::TARGET_FUNCTION)]
class ValidateArgs implements AttributeInterface
{
public function before(Context $ctx): void
{
$params = $ctx->getParams();
foreach ($params as $i => $value) {
if ($value === null) {
throw new \InvalidArgumentException(
"Argument {$i} of {$ctx->target} must not be null"
);
}
}
}
public function after(Context $ctx): void {}
}多个装饰器
可以在同一个函数上叠加多个装饰器。对于 before() 它们按声明顺序执行,对于 after() 则按相反顺序执行:
<?php
#[RateLimit(maxCalls: 100, windowSeconds: 60)]
#[Timer]
#[Cache(ttl: 300)]
function getProduct(int $id): array
{
// Execution order:
// 1. RateLimit::before()
// 2. Timer::before()
// 3. Cache::before()
// 4. getProduct() executes
// 5. Cache::after()
// 6. Timer::after()
// 7. RateLimit::after()
}如果 before() 抛出异常,OxPHP 会记录该异常,并对所有已经完成其 before() 的装饰器按相反顺序调用 after()。调用方将通过常规的 PHP try/catch 观察到该异常,而抛出异常的那个装饰器的 after() 不会被调用。
PHP 的 zend_observer_fcall_begin API——OxPHP 用它来调用 before()——并不提供取消调用本身的途径。当 before() 抛出异常时,在 VM 展开到最近的异常处理器之前,被装饰的函数体可能仍会执行少量操作码。不要依赖 RejectedException 来跳过函数体内的副作用。 应将装饰器的拒绝视为“调用方看到一个异常”,并在函数体内部(或它之前)实现任何硬性授权关卡,而不是在装饰器里实现。
中止执行
装饰器可以通过在 before() 中抛出 OxPHP\Decorator\RejectedException 来发出拒绝信号。该异常会传播给调用方,但(如上所述)它并不是一个调用前的否决:
<?php
#[Attribute(Attribute::TARGET_METHOD)]
class RequireRole implements AttributeInterface
{
public function __construct(
public readonly string $role,
) {}
public function before(Context $ctx): void
{
if (!current_user_has_role($this->role)) {
throw new \OxPHP\Decorator\RejectedException(
"Access denied: requires role '{$this->role}'"
);
}
}
public function after(Context $ctx): void {}
}工作进程模式下的行为
在工作进程模式下,PHP 进程会跨请求持续存在,但装饰器实例不会:每个工作进程的实例缓存会在每次请求结束时被清空。这意味着:
- 构造函数逻辑在每次请求中、针对每个被装饰的函数只运行一次——即在该函数于本次请求内首次被调用时——而不是在工作进程的整个生命周期内只运行一次
- 属性中的实例状态由单次请求内对某个被装饰函数的每次调用共享,但不会延续到下一次请求
- 每次请求都以一个全新的实例开始(并伴随一次全新的构造函数运行,重新求值属性参数)
请将装饰器设计为在请求之间无状态。如果你需要每次请求的状态,请在 before() 中设置它,并在 after() 中读取它:
<?php
#[Attribute(Attribute::TARGET_METHOD)]
class RequestTimer implements AttributeInterface
{
// Per-request state: set in before(), read in after()
private float $start;
public function before(Context $ctx): void
{
$this->start = hrtime(true);
}
public function after(Context $ctx): void
{
$elapsed = (hrtime(true) - $this->start) / 1e6;
// Safe: $this->start is always set fresh in before()
}
}内置装饰器
#[OxPHP\Apm\Trace]
当启用 APM 插件时(OTEL_APM_ENABLED=true),OxPHP 会为 #[OxPHP\Apm\Trace] 属性注册一个内置装饰器。它会在函数进入时自动创建一个 span,并在退出时关闭它——无需手动调用 oxphp_apm_start() / oxphp_apm_end()。
<?php
use OxPHP\Apm\Trace;
#[Trace]
function processOrder(int $orderId): void
{
// A span named "processOrder" is created automatically.
// If this function throws, the span is marked as error
// and an "exception" event is recorded with the class name.
}
class PaymentService
{
#[Trace]
public function charge(float $amount): bool
{
// Span named "PaymentService::charge"
return true;
}
}#[Trace] 属性同时以函数和方法为目标。它适用于用户定义的 PHP 代码(而非内部 C 函数——那些由 APM 自动埋点钩子处理)。
无需调用 oxphp_register_decorator()——APM 插件会在服务器初始化期间自动注册该装饰器。该装饰器在标准模式和工作进程模式下均可用。
关于 APM 追踪的更多内容,请参阅 分布式追踪与 APM。
局限性
- 仅限用户函数——内置 PHP 函数无法被装饰。只有在 PHP 代码中定义的函数和方法才可被拦截
- 必须在首次调用前注册——装饰器必须在其目标的任何函数首次被调用之前注册。请在引导阶段注册
- 仅限标量构造函数参数——属性的构造函数参数在首次调用时求值一次。属性中不支持复杂表达式或运行时值
- 最多 256 层嵌套——装饰器上下文栈最多支持 256 层嵌套的被装饰函数调用。超过这一层数,调用会抛出
OxPHP\Decorator\StackOverflowException,而不是悄无声息地破坏装饰器上下文
故障排查
装饰器未拦截调用
装饰器是在函数已经被调用之后才注册的,或者该属性未被识别。
检查: 确保 oxphp_register_decorator() 在任何被装饰的函数被调用之前被调用。在工作进程模式下,请在 oxphp_worker() 之前的外层作用域中注册。
注册时出现“Class not found”
在调用 oxphp_register_decorator() 时,装饰器类尚未被加载。
修复: 确保自动加载器先被注册:
<?php
require __DIR__ . '/../vendor/autoload.php';
// Now the class can be found
oxphp_register_decorator(Timer::class);构造函数参数在请求之间不更新
属性参数是源代码级别的字面量,在装饰器实例被构造时(每次请求中的首次调用)求值。它们不是运行时值,因此变化的应用数据无法改变它们——它们只有在你编辑代码中的属性时才会改变。
修复: 使用 before() 进行每次请求的初始化,而不是构造函数。构造函数应只接受属性中声明的静态配置。
PHP 示例
缓存装饰器
<?php
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
class Cache implements AttributeInterface
{
private static array $store = [];
public function __construct(
public readonly int $ttl = 60,
) {}
public function before(Context $ctx): void
{
$key = $ctx->target . ':' . serialize($ctx->getParams());
if (isset(self::$store[$key]) && self::$store[$key]['expires'] > time()) {
// Skip function execution — return cached value
// Note: you cannot short-circuit execution from PHP decorators.
// Use this pattern with an external cache check in the function itself.
}
}
public function after(Context $ctx): void
{
if ($ctx->hasResult()) {
$key = $ctx->target . ':' . serialize($ctx->getParams());
self::$store[$key] = [
'value' => $ctx->getResult(),
'expires' => time() + $this->ttl,
];
}
}
}带请求上下文的日志装饰器
<?php
#[Attribute(Attribute::TARGET_METHOD)]
class LogCall implements AttributeInterface
{
public function before(Context $ctx): void
{
error_log(json_encode([
'event' => 'call_start',
'target' => $ctx->target,
'request_id' => $ctx->requestId,
'params' => $ctx->getParams(),
]));
}
public function after(Context $ctx): void
{
error_log(json_encode([
'event' => 'call_end',
'target' => $ctx->target,
'request_id' => $ctx->requestId,
'success' => $ctx->hasResult(),
]));
}
}