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, l'état PHP persiste d'une requête à l'autre, et les instances de décorateur persistent avec lui. Chaque thread worker met en cache une instance par fonction décorée et par attribut, et ne vide ce cache qu'à l'arrêt du worker. Cela signifie que :

  • La logique du constructeur s'exécute une fois par worker et par fonction décorée — au premier appel de cette fonction sur le worker — et non une fois par requête
  • L'état d'instance stocké dans les propriétés est reporté d'une requête à la suivante, et les Fibers de requête qui s'exécutent simultanément sur le même worker le partagent
  • Seul un nouveau worker, après un recyclage ou un redémarrage, construit une instance neuve et réévalue les arguments de l'attribut

Ne stockez pas d'état propre à la requête dans les propriétés d'un décorateur. Une propriété définie dans before() et lue dans after(), comme ci-dessous, ne reste correcte que si l'appel décoré ne se suspend pas entre les deux : pendant qu'une Fiber de requête attend, une autre requête peut appeler la même fonction et l'écraser.

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

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 une requête ou, en mode worker, le premier appel sur ce worker). 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(), ])); } }

Prompts pour votre agent

Ces prompts supposent un agent connecté au serveur MCP de la documentation OxPHP — il lira lui-même les pages dont il a besoin.

prompt
Remplace le code transversal de mes classes par un décorateur OxPHP. Lis la page `features/decorators` via le serveur MCP de la documentation OxPHP. Trouve dans ce dépôt les méthodes où le chronométrage, la journalisation ou un contrôle d'accès est copié-collé dans le corps, écris l'attribut et la classe de décorateur qui l'en extraient, et montre-moi où l'enregistrer dans mon bootstrap pour qu'il soit en place avant le premier appel.

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
Une erreur ? Signalez-la →