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
- Définissez une classe de décorateur qui implémente
OxPHP\Decorator\AttributeInterfaceet qui est annotée avec#[Attribute]. - Enregistrez-la une seule fois au démarrage avec
oxphp_register_decorator(ClassName::class). - Appliquez l'attribut à n'importe quelle fonction, méthode ou classe.
- Au premier appel d'une fonction décorée, OxPHP détecte l'attribut et installe les points d'interception.
- À chaque appel suivant,
before()s'exécute avant la fonction etafter()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
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
require __DIR__ . '/../vendor/autoload.php';
oxphp_register_decorator(Timer::class);Appliquez-le aux fonctions et aux méthodes :
<?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
#[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
#[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
#[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.
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
#[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
#[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
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\StackOverflowExceptionplutô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
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
#[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
#[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.
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