Décorateurs

Les décorateurs OxPHP interceptent les appels de fonctions et de méthodes PHP à l'aide des attributs PHP 8. Ajoutez un attribut à n'importe quelle fonction ou méthode, et OxPHP appelle les méthodes before() et after() de votre décorateur autour de chaque invocation, sans modifier le code d'origine.

Fonctionnement

  1. Définissez une classe de décorateur qui implémente OxPHP\Decorator\AttributeInterface et qui est annotée avec #[Attribute].
  2. Enregistrez-la une seule fois au démarrage avec oxphp_register_decorator(ClassName::class).
  3. Appliquez l'attribut à n'importe quelle fonction, méthode ou classe.
  4. Au premier appel d'une fonction décorée, OxPHP détecte l'attribut et installe les points d'interception.
  5. À chaque appel suivant, before() s'exécute avant la fonction et after() s'exécute après son retour.

Écrire un décorateur

Une classe de décorateur a besoin de deux choses : l'annotation #[Attribute] et l'implémentation de 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)); } }

Enregistrez le décorateur au démarrage, avant tout appel à une fonction décorée :

php
<?php require __DIR__ . '/../vendor/autoload.php'; oxphp_register_decorator(Timer::class);

Appliquez-le aux fonctions et aux méthodes :

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 { // ... } }

Décorateurs au niveau de la classe

Appliquez un attribut à une classe pour décorer toutes ses méthodes :

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 { /* ... */ } }

L'objet Context

before() et after() reçoivent tous deux un objet OxPHP\Decorator\Context contenant des informations sur l'appel décoré.

Propriétés

Propriété Type Description
$target string Nom complet de la cible : App\Service::method ou my_function
$class string Nom de la classe, ou "" pour les fonctions autonomes
$method string Nom de la méthode, ou "" pour les fonctions autonomes
$function string Nom de la fonction pour les fonctions autonomes, ou "" pour les méthodes
$objectId int spl_object_id() de l'objet appelé, 0 pour les fonctions et les méthodes statiques
$requestId string ID de requête courant
$traceId string ID de trace W3C courant. Chaîne vide lorsque le traçage distribué n'est pas actif

Méthodes

Méthode Disponible dans Description
getParams(): array before() et after() Arguments passés à la fonction décorée
getResult(): mixed after() uniquement Valeur de retour de la fonction décorée. Renvoie null dans before() ou après une exception
hasResult(): bool after() uniquement true si la fonction s'est terminée correctement sans lever d'exception

Inspecter les arguments

getParams() renvoie les arguments sous forme de tableau indexé numériquement :

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 {} }

Plusieurs décorateurs

Empilez plusieurs décorateurs sur une même fonction. Ils s'exécutent dans l'ordre de déclaration pour before() et dans l'ordre inverse pour 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() }

Si before() lève une exception, OxPHP enregistre l'exception et invoque after() dans l'ordre inverse sur tous les décorateurs qui ont déjà terminé leur before(). L'appelant observera l'exception via les blocs try/catch PHP habituels, et after() n'est pas appelé sur le décorateur dont le before() a levé l'exception.

Réserve importante à propos de l'arrêt de l'exécution

L'API zend_observer_fcall_begin de PHP — que OxPHP utilise pour invoquer before() — n'offre aucun moyen d'annuler l'appel lui-même. Lorsque before() lève une exception, le corps de la fonction décorée peut encore exécuter quelques opcodes avant que la VM ne remonte jusqu'au gestionnaire d'exception le plus proche. Ne comptez pas sur RejectedException pour ignorer les effets de bord à l'intérieur du corps de la fonction. Considérez le rejet d'un décorateur comme « l'appelant reçoit une exception » et implémentez toute barrière d'autorisation stricte à l'intérieur du corps de la fonction (ou en amont), et non dans le décorateur.

Arrêter l'exécution

Un décorateur peut signaler un rejet en levant OxPHP\Decorator\RejectedException depuis before(). L'exception se propage jusqu'à l'appelant, mais (comme indiqué ci-dessus) il ne s'agit pas d'un veto préalable à l'appel :

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 {} }

Comportement en mode worker

En mode worker, le processus PHP persiste d'une requête à l'autre, mais pas les instances de décorateur : le cache d'instances propre à chaque worker est vidé à la fin de chaque requête. Cela signifie que :

  • La logique du constructeur s'exécute une fois par requête et par fonction décorée — au premier appel de cette fonction au sein de la requête — et non une seule fois pour toute la durée de vie du worker
  • L'état d'instance stocké dans les propriétés est partagé par tous les appels d'une fonction décorée au sein d'une même requête, mais n'est pas reporté à la requête suivante
  • Chaque requête démarre avec une instance neuve (et une nouvelle exécution du constructeur, qui réévalue les arguments de l'attribut)

Concevez les décorateurs pour qu'ils soient sans état entre les requêtes. Si vous avez besoin d'un état propre à la requête, définissez-le dans before() et lisez-le dans after() :

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

Décorateurs intégrés

#[OxPHP\Apm\Trace]

Lorsque le plugin APM est activé (OTEL_APM_ENABLED=true), OxPHP enregistre un décorateur intégré pour l'attribut #[OxPHP\Apm\Trace]. Il crée automatiquement un span à l'entrée de la fonction et le ferme à la sortie — aucun appel manuel à oxphp_apm_start() / oxphp_apm_end() n'est nécessaire.

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; } }

L'attribut #[Trace] cible à la fois les fonctions et les méthodes. Il fonctionne sur du code PHP défini par l'utilisateur (pas sur les fonctions C internes — celles-ci sont prises en charge par les hooks d'auto-instrumentation de l'APM).

Aucun appel à oxphp_register_decorator() n'est nécessaire — le plugin APM enregistre ce décorateur automatiquement lors de l'initialisation du serveur. Le décorateur est disponible aussi bien en mode standard qu'en mode worker.

Pour en savoir plus sur le traçage APM, consultez Traçage distribué et APM.

Limitations

  • Fonctions utilisateur uniquement — les fonctions PHP intégrées ne peuvent pas être décorées. Seules les fonctions et méthodes définies dans du code PHP sont interceptables
  • Enregistrement avant le premier appel — les décorateurs doivent être enregistrés avant la première invocation de toute fonction qu'ils ciblent. Enregistrez-les au démarrage
  • Arguments de constructeur scalaires — les arguments du constructeur d'attribut sont évalués une seule fois, au premier appel. Les expressions complexes ou les valeurs déterminées à l'exécution dans les attributs ne sont pas prises en charge
  • 256 niveaux d'imbrication maximum — la pile de contexte des décorateurs prend en charge jusqu'à 256 niveaux d'appels de fonctions décorées imbriquées. Au-delà, l'appel lève OxPHP\Decorator\StackOverflowException plutôt que de corrompre silencieusement le contexte du décorateur

Dépannage

Le décorateur n'intercepte pas les appels

Le décorateur a été enregistré après que la fonction a déjà été appelée, ou l'attribut n'est pas reconnu.

À vérifier : assurez-vous que oxphp_register_decorator() est appelé avant toute invocation d'une fonction décorée. En mode worker, enregistrez-le dans la portée externe, avant oxphp_worker().

« Class not found » lors de l'enregistrement

La classe du décorateur n'est pas chargée au moment de l'appel à oxphp_register_decorator().

Correctif : assurez-vous que l'autoloader est enregistré en premier :

php
<?php require __DIR__ . '/../vendor/autoload.php'; // Now the class can be found oxphp_register_decorator(Timer::class);
Les arguments du constructeur ne se mettent pas à jour entre les requêtes

Les arguments d'attribut sont des littéraux au niveau du code source, évalués lors de la construction de l'instance du décorateur (le premier appel dans chaque requête). Ce ne sont pas des valeurs déterminées à l'exécution ; par conséquent, la mutation des données applicatives ne peut pas les modifier — ils ne changent que lorsque vous éditez l'attribut dans le code.

Correctif : utilisez before() pour l'initialisation propre à la requête, pas le constructeur. Le constructeur ne devrait accepter que la configuration statique déclarée dans l'attribut.

Exemples PHP

Décorateur de mise en cache

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, ]; } } }

Décorateur de journalisation avec contexte de requête

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(), ])); } }

Voir aussi

  • Fonctions PHP -- référence de oxphp_register_decorator()
  • Mode worker -- comment les workers persistants influent sur la durée de vie des instances de décorateur
  • Traçage distribué -- utiliser des décorateurs avec le contexte de trace pour des spans personnalisés