Декораторы
Декораторы 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
{
// Shared by every call on this worker: 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;
// Wrong if another call ran before() while this one was suspended
}
}Встроенные декораторы
#[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(),
]));
}
}Промпты для вашего агента
Эти промпты рассчитаны на агента с подключённым MCP-сервером документации OxPHP — нужные страницы он прочитает сам.
Замени сквозной код в моих классах декоратором OxPHP. Прочитай страницу `features/decorators` через MCP-сервер документации OxPHP.
Найди в этом репозитории методы, в тело которых скопированы замер времени, логирование или проверка доступа, напиши атрибут и класс декоратора, которые это выносят, и покажи, где в моей инициализации его зарегистрировать, чтобы он был на месте до первого вызова.Смотрите также
- Функции PHP -- справочник по
oxphp_register_decorator() - Режим воркеров -- как персистентные воркеры влияют на время жизни экземпляра декоратора
- Распределённая трассировка -- использование декораторов с контекстом трассировки для пользовательских спанов