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