Декораторы
Декораторы OxPHP перехватывают вызовы функций и методов PHP с помощью атрибутов PHP 8. Добавьте атрибут к любой функции или методу — и 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 |
Идентификатор текущего запроса |
$traceId |
string |
Текущий идентификатор трассировки W3C. Пустая строка, если распределённая трассировка не активна |
Методы
| Метод | Доступен в | Описание |
|---|---|---|
getParams(): array |
before() и after() |
Аргументы, переданные декорированной функции |
getResult(): mixed |
только after() |
Возвращаемое значение декорированной функции. Возвращает null в before() или после исключения |
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 записывает это исключение и вызывает after() в обратном порядке для всех декораторов, которые уже завершили свой before(). Вызывающий код увидит исключение через обычный PHP try/catch, а after() не вызывается для декоратора, чей before() выбросил исключение.
API zend_observer_fcall_begin в PHP — которое OxPHP использует для вызова before() — не предоставляет способа отменить сам вызов. Когда before() выбрасывает исключение, тело декорированной функции всё ещё может выполнить некоторое количество опкодов, прежде чем виртуальная машина развернёт стек до ближайшего обработчика исключений. Не полагайтесь на RejectedException для пропуска побочных эффектов внутри тела функции. Рассматривайте отклонение декоратора как «вызывающий код видит исключение» и реализуйте любой жёсткий барьер авторизации внутри тела функции (или перед ней), а не в декораторе.
Остановка выполнения
Декоратор может сигнализировать об отклонении, выбросив OxPHP\Decorator\RejectedException из before(). Исключение распространяется до вызывающего кода, но (как отмечено выше) это не вето перед вызовом:
<?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]. Он автоматически создаёт спан при входе в функцию и закрывает его при выходе — без необходимости в ручных вызовах 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(),
]));
}
}Смотрите также
- Функции PHP -- справочник по
oxphp_register_decorator() - Режим воркеров -- как персистентные воркеры влияют на время жизни экземпляра декоратора
- Распределённая трассировка -- использование декораторов с контекстом трассировки для пользовательских спанов