Декоратори
Декоратори 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() уже завершився. Код, що викликає функцію, отримає виняток через звичайні try/catch PHP, а для декоратора, чий before() згенерував виняток, after() не викликається.
API PHP zend_observer_fcall_begin, через який 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]. Він автоматично створює 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(),
]));
}
}Промпти для вашого агента
Ці промпти розраховані на агента з підключеним MCP-сервером документації OxPHP — потрібні сторінки він прочитає сам.
Заміни наскрізний код у моїх класах декоратором OxPHP. Прочитай сторінку `features/decorators` через MCP-сервер документації OxPHP.
Знайди в цьому репозиторії методи, у тіло яких скопійовано вимірювання часу, логування чи перевірку доступу, напиши атрибут і клас декоратора, які винесуть цей код назовні, і покажи, де в моєму початковому завантаженні його зареєструвати, щоб він був на місці до першого виклику.Дивіться також
- Функції PHP -- довідник з
oxphp_register_decorator() - Режим воркерів -- як постійні воркери впливають на час життя екземпляра декоратора
- Розподілене трасування -- використання декораторів із контекстом трасування для власних spanʼів