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

  1. Zdefiniuj klasę dekoratora, która implementuje OxPHP\Decorator\AttributeInterface i jest oznaczona adnotacją #[Attribute].
  2. Zarejestruj ją raz podczas bootstrapu za pomocą oxphp_register_decorator(ClassName::class).
  3. Zastosuj atrybut do dowolnej funkcji, metody lub klasy.
  4. Przy pierwszym wywołaniu udekorowanej funkcji OxPHP wykrywa atrybut i instaluje haki przechwytujące.
  5. Przy każdym kolejnym wywołaniu before() uruchamia się przed funkcją, a after() po jej powrocie.

Pisanie dekoratora

Klasa dekoratora potrzebuje dwóch rzeczy: adnotacji #[Attribute] oraz implementacji AttributeInterface.

php
<?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
<?php require __DIR__ . '/../vendor/autoload.php'; oxphp_register_decorator(Timer::class);

Zastosuj go do funkcji i metod:

php
<?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
<?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
<?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
<?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.

Ważne zastrzeżenie dotyczące zatrzymywania wykonania

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
<?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 stan PHP trwa pomiędzy żądaniami, a wraz z nim trwają instancje dekoratorów. Każdy wątek workera przechowuje w cache jedną instancję na każdą udekorowaną funkcję i atrybut, a opróżnia ten cache dopiero wtedy, gdy worker kończy działanie. Oznacza to, że:

  • Logika konstruktora uruchamia się raz na workera dla każdej udekorowanej funkcji — przy jej pierwszym wywołaniu na danym workerze — a nie raz na żądanie
  • Stan instancji w jej właściwościach przenosi się z jednego żądania do następnego, a fibery żądań działające współbieżnie na tym samym workerze go współdzielą
  • Dopiero nowy worker, po recyklingu lub restarcie, tworzy świeżą instancję i ponownie ewaluuje argumenty atrybutu

Nie trzymaj stanu per-żądanie we właściwościach dekoratora. Właściwość ustawiona w before() i odczytana w after(), jak poniżej, pozostaje poprawna tylko wtedy, gdy udekorowane wywołanie nie zawiesza się pomiędzy nimi: gdy fiber jednego żądania czeka, inne żądanie może wywołać tę samą funkcję i ją nadpisać.

php
<?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 } }

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
<?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
<?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 żądaniu, a w trybie worker pierwsze wywołanie na danym workerze). 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
<?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
<?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(), ])); } }

Prompty dla Twojego agenta

Te prompty zakładają agenta z podłączonym serwerem MCP dokumentacji OxPHP — sam przeczyta strony, których potrzebuje.

prompt
Zastąp przekrojowy kod w moich klasach dekoratorem OxPHP. Przeczytaj stronę `features/decorators` przez serwer MCP dokumentacji OxPHP. Znajdź w tym repozytorium metody, w których pomiar czasu, logowanie albo kontrola dostępu są skopiowane wprost do ciała, napisz atrybut i klasę dekoratora, które to stamtąd wyniosą, i pokaż mi, w którym miejscu bootstrapu go zarejestrować, żeby był na miejscu przed pierwszym wywołaniem.

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
Znalazłeś błąd? Zgłoś go →