Dekoratory
Dekoratory OxPHP przechwytują wywołania funkcji i metod PHP za pomocą atrybutów PHP 8. Dodaj atrybut do dowolnej funkcji lub metody, a OxPHP będzie wokół każdego wywołania uruchamiać metody before() i after() twojego dekoratora — bez zmiany oryginalnego kodu.
Jak to działa
- Zdefiniuj klasę dekoratora, która implementuje
OxPHP\Decorator\AttributeInterfacei jest oznaczona adnotacją#[Attribute]. - Zarejestruj ją raz podczas bootstrapu za pomocą
oxphp_register_decorator(ClassName::class). - Zastosuj atrybut do dowolnej funkcji, metody lub klasy.
- Przy pierwszym wywołaniu udekorowanej funkcji OxPHP wykrywa atrybut i instaluje haki przechwytujące.
- Przy każdym kolejnym wywołaniu
before()uruchamia się przed funkcją, aafter()po jej powrocie.
Pisanie dekoratora
Klasa dekoratora potrzebuje dwóch rzeczy: adnotacji #[Attribute] oraz implementacji 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));
}
}Zarejestruj dekorator podczas bootstrapu, zanim zostanie wywołana jakakolwiek udekorowana funkcja:
<?php
require __DIR__ . '/../vendor/autoload.php';
oxphp_register_decorator(Timer::class);Zastosuj go do funkcji i metod:
<?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
{
// ...
}
}Dekoratory na poziomie klasy
Zastosuj atrybut do klasy, aby udekorować wszystkie jej metody:
<?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 { /* ... */ }
}Obiekt Context
Zarówno before(), jak i after() otrzymują obiekt OxPHP\Decorator\Context z informacjami o udekorowanym wywołaniu.
Właściwości
| Właściwość | Typ | Opis |
|---|---|---|
$target |
string |
Pełna nazwa celu: App\Service::method lub my_function |
$class |
string |
Nazwa klasy lub "" dla samodzielnych funkcji |
$method |
string |
Nazwa metody lub "" dla samodzielnych funkcji |
$function |
string |
Nazwa funkcji dla samodzielnych funkcji lub "" dla metod |
$objectId |
int |
spl_object_id() wywołanego obiektu, 0 dla funkcji i metod statycznych |
$requestId |
string |
Bieżący ID żądania |
$traceId |
string |
Bieżący identyfikator trace W3C. Pusty łańcuch, gdy śledzenie rozproszone nie jest aktywne |
Metody
| Metoda | Dostępna w | Opis |
|---|---|---|
getParams(): array |
before() i after() |
Argumenty przekazane do udekorowanej funkcji |
getResult(): mixed |
tylko after() |
Wartość zwracana przez udekorowaną funkcję. Zwraca null w before() lub po wyjątku |
hasResult(): bool |
tylko after() |
true, jeśli funkcja zwróciła wartość pomyślnie, bez rzucania wyjątku |
Inspekcja argumentów
getParams() zwraca argumenty jako tablicę indeksowaną numerycznie:
<?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 {}
}Wiele dekoratorów
Można nałożyć wiele dekoratorów na tę samą funkcję. Wykonują się w kolejności deklaracji dla before() i w odwrotnej kolejności dla 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()
}Jeśli before() rzuci wyjątek, OxPHP rejestruje ten wyjątek i wywołuje after() w odwrotnej kolejności na wszystkich dekoratorach, które już zakończyły swoje before(). Wywołujący zobaczy wyjątek przez zwykłe PHP-owe try/catch, a after() nie jest wywoływane na dekoratorze, którego before() rzuciło wyjątek.
API PHP zend_observer_fcall_begin — którego OxPHP używa do wywoływania before() — nie udostępnia sposobu na anulowanie samego wywołania. Gdy before() rzuci wyjątek, ciało udekorowanej funkcji może wciąż wykonać garść opcodów, zanim VM rozwinie stos do najbliższego handlera wyjątków. Nie polegaj na RejectedException, aby pominąć efekty uboczne wewnątrz ciała funkcji. Traktuj odrzucenie przez dekorator jako „wywołujący widzi wyjątek” i zaimplementuj każdą twardą bramkę autoryzacji wewnątrz ciała funkcji (lub przed nim), a nie w dekoratorze.
Zatrzymywanie wykonania
Dekorator może zasygnalizować odrzucenie, rzucając OxPHP\Decorator\RejectedException z before(). Wyjątek propaguje się do wywołującego, ale (jak zaznaczono powyżej) nie jest to weto przed wywołaniem:
<?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 {}
}Zachowanie w trybie worker
W trybie worker proces PHP trwa pomiędzy żądaniami, ale instancje dekoratorów już nie: cache instancji per-worker jest czyszczony na końcu każdego żądania. Oznacza to, że:
- Logika konstruktora uruchamia się raz na żądanie dla każdej udekorowanej funkcji — przy jej pierwszym wywołaniu w obrębie żądania — a nie raz na cały cykl życia workera
- Stan instancji w jej właściwościach jest współdzielony przez każde wywołanie udekorowanej funkcji w obrębie pojedynczego żądania, ale nie przenosi się do następnego żądania
- Każde żądanie zaczyna się od świeżej instancji (i świeżego uruchomienia konstruktora, ponownie ewaluującego argumenty atrybutu)
Projektuj dekoratory tak, aby były bezstanowe pomiędzy żądaniami. Jeśli potrzebujesz stanu per-żądanie, ustaw go w before() i odczytaj w 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()
}
}Wbudowane dekoratory
#[OxPHP\Apm\Trace]
Gdy wtyczka APM jest włączona (OTEL_APM_ENABLED=true), OxPHP rejestruje wbudowany dekorator dla atrybutu #[OxPHP\Apm\Trace]. Automatycznie tworzy span przy wejściu do funkcji i zamyka go przy wyjściu — bez potrzeby ręcznych wywołań 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;
}
}Atrybut #[Trace] celuje zarówno w funkcje, jak i metody. Działa na kodzie PHP zdefiniowanym przez użytkownika (nie na wewnętrznych funkcjach C — tymi zajmują się haki automatycznej instrumentacji APM).
Nie jest potrzebne żadne wywołanie oxphp_register_decorator() — wtyczka APM rejestruje ten dekorator automatycznie podczas inicjalizacji serwera. Dekorator jest dostępny zarówno w trybie standardowym, jak i w trybie worker.
Więcej o śledzeniu APM znajdziesz w Śledzenie rozproszone i APM.
Ograniczenia
- Tylko funkcje użytkownika — wbudowanych funkcji PHP nie można dekorować. Przechwytywalne są tylko funkcje i metody zdefiniowane w kodzie PHP
- Rejestracja przed pierwszym wywołaniem — dekoratory muszą zostać zarejestrowane przed pierwszym wywołaniem jakiejkolwiek funkcji, w którą celują. Rejestruj je podczas bootstrapu
- Skalarne argumenty konstruktora — argumenty konstruktora atrybutu są ewaluowane raz, przy pierwszym wywołaniu. Złożone wyrażenia lub wartości ustalane w czasie działania w atrybutach nie są obsługiwane
- Maksymalnie 256 poziomów zagnieżdżenia — stos kontekstu dekoratorów obsługuje do 256 poziomów zagnieżdżonych wywołań udekorowanych funkcji. Powyżej tego wywołanie rzuca
OxPHP\Decorator\StackOverflowException, zamiast po cichu uszkodzić kontekst dekoratora
Rozwiązywanie problemów
Dekorator nie przechwytuje wywołań
Dekorator został zarejestrowany po tym, jak funkcja została już wywołana, albo atrybut nie jest rozpoznawany.
Sprawdź: Upewnij się, że oxphp_register_decorator() jest wywoływane przed wywołaniem jakiejkolwiek udekorowanej funkcji. W trybie worker rejestruj w zakresie zewnętrznym, przed oxphp_worker().
„Class not found” podczas rejestracji
Klasa dekoratora nie jest załadowana w momencie wywołania oxphp_register_decorator().
Rozwiązanie: Upewnij się, że autoloader jest zarejestrowany jako pierwszy:
<?php
require __DIR__ . '/../vendor/autoload.php';
// Now the class can be found
oxphp_register_decorator(Timer::class);Argumenty konstruktora nie aktualizują się między żądaniami
Argumenty atrybutu to literały na poziomie kodu źródłowego, ewaluowane w momencie konstruowania instancji dekoratora (pierwsze wywołanie w każdym żądaniu). Nie są to wartości ustalane w czasie działania, więc zmieniające się dane aplikacji nie mogą ich zmienić — zmieniają się tylko wtedy, gdy edytujesz atrybut w kodzie.
Rozwiązanie: Użyj before() do inicjalizacji per-żądanie, a nie konstruktora. Konstruktor powinien przyjmować wyłącznie statyczną konfigurację zadeklarowaną w atrybucie.
Przykłady w PHP
Dekorator cache'ujący
<?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,
];
}
}
}Dekorator logujący z kontekstem żądania
<?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(),
]));
}
}Zobacz też
- Funkcje PHP -- dokumentacja
oxphp_register_decorator() - Tryb worker -- jak trwałe workery wpływają na cykl życia instancji dekoratora
- Śledzenie rozproszone -- używanie dekoratorów z kontekstem trace dla niestandardowych spanów