WordPress, c’est fantastique… mais parfois, le système de hooks natif montre ses limites quand on veut créer des applications complexes et maintenables. Et si je vous disais qu’il était possible de créer votre propre système d’événements, plus flexible et scalable ? Aujourd’hui, on va voir comment implémenter un Event Manager personnalisé qui va révolutionner votre façon de structurer vos développements WordPress.
Comprendre l’architecture événementielle dans WordPress
Quand on commence à développer sur WordPress, le système de hooks semble magique. On ajoute quelques add_action et add_filter, et hop ! Notre code s’intègre parfaitement. Mais voilà… au fur et à mesure que nos projets grossissent, on commence à voir les limites. Et c’est là qu’une approche événementielle devient intéressante.
Les limites du système de hooks natif
Bon, soyons honnêtes : le système de hooks WordPress est génial… pour des besoins simples. Mais dès qu’on se retrouve avec plusieurs plugins qui interagissent, ou un thème complexe avec plein de fonctionnalités, ça devient le bazar.
Premier problème : les plugins qui se marchent dessus. J’ai déjà vu des sites où trois plugins différents modifiaient le même hook avec des priorités qui s’entremêlaient. Résultat ? Un comportement imprévisible selon l’ordre d’activation des plugins.
Ensuite, il y a les dépendances circulaires. Plugin A a besoin que Plugin B soit initialisé, mais Plugin B attend quelque chose de Plugin A. Avec les hooks classiques, on se retrouve souvent à bidouiller avec les priorités ou à créer des vérifications partout.
Et puis, côté performance : certains hooks WordPress sont appelés des centaines de fois par page. Si vous y attachez des fonctions lourdes, votre site rame.
Principes de l’Event-Driven Architecture
L’Event-Driven Architecture (EDA), c’est un peu comme organiser une soirée. Au lieu que tout le monde se parle directement (et crée un brouhaha), on a un maître de cérémonie qui annonce les événements : « Le buffet est servi ! », « La musique change ! », « Il est temps de partir ! ».
Dans notre contexte WordPress, ça donne ça : au lieu que les composants s’appellent directement, ils émettent des événements (UserRegistered, OrderCompleted, PostPublished) et d’autres composants écoutent ces événements.
Le découplage, c’est le maître-mot. Votre module de newsletters n’a pas besoin de connaître votre système d’e-commerce : il écoute juste l’événement UserRegistered et fait son travail. Clean, non ?
Pour la scalabilité, c’est encore mieux. Vous voulez ajouter un système de points de fidélité ? Vous créez un nouveau listener sur OrderCompleted. Pas besoin de modifier le code existant !
Avantages d’un système d’événements custom
Alors, pourquoi se compliquer la vie avec un système custom au lieu d’utiliser les hooks WordPress ?
D’abord, la lisibilité. Plutôt que d’avoir des do_action('some_obscure_hook_name') éparpillés partout, vous avez des événements nommés explicitement : new UserRegisteredEvent($user). C’est plus clair, non ?
Ensuite, la robustesse. Avec un système d’événements bien conçu, vous pouvez gérer les erreurs proprement. Un listener qui plante ? Les autres continuent de fonctionner. Avec les hooks WordPress, une function mal codée peut faire planter toute la chaîne.
Et niveau debugging : c’est le jour et la nuit ! Vous pouvez logger tous les événements, voir qui écoute quoi, tracer le flux d’exécution. Fini les var_dump pour comprendre pourquoi tel hook ne se déclenche pas.
Enfin, la testabilité. Tester du code qui dépend des hooks WordPress, c’est… sportif. Avec des événements, vous moquez votre dispatcher et vous testez vos listeners indépendamment. Beaucoup plus clean !
Créer son Event Manager personnalisé
Maintenant qu’on a vu les limites du système WordPress, passons à la pratique ! Je vais vous montrer comment développer un Event Manager complet qui va révolutionner votre façon de gérer les événements.
Architecture de base : singleton ou dependency injection ?
Bon, première question épineuse : comment structurer notre Event Manager ? J’ai longtemps hésité entre le pattern singleton (pratique mais critiqué) et l’injection de dépendances (plus propre mais plus complexe).
Voici les deux approches :
// Approche Singleton (plus simple)
class EventManager {
private static $instance = null;
private $listeners = [];
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
}
// Approche Dependency Injection (plus flexible)
class EventManager {
private $listeners = [];
public function __construct() {
// Injection via constructeur ou setter
}
}
Personnellement, je recommande l’injection de dépendances pour les projets complexes, mais le singleton reste valable pour débuter.
Implémentation complète avec méthodes emit(), on(), off()
Voici notre Event Manager complet avec toutes les fonctionnalités essentielles :
class EventManager {
private $listeners = [];
private $wildcards = [];
/**
* Écouter un événement avec priorité
*/
public function on($event, $callback, $priority = 10) {
// Gestion des wildcards (ex: 'user.*')
if (strpos($event, '*') !== false) {
$this->wildcards[$event][] = [
'callback' => $callback,
'priority' => $priority
];
return;
}
// Événements normaux
if (!isset($this->listeners[$event])) {
$this->listeners[$event] = [];
}
$this->listeners[$event][] = [
'callback' => $callback,
'priority' => $priority,
'id' => uniqid() // Pour pouvoir remove plus tard
];
// Tri par priorité (plus élevé = exécuté en premier)
usort($this->listeners[$event], function($a, $b) {
return $b['priority'] - $a['priority'];
});
}
/**
* Déclencher un événement
*/
public function emit($event, $data = null, $propagate = true) {
$result = true;
// Exécution des listeners normaux
if (isset($this->listeners[$event])) {
foreach ($this->listeners[$event] as $listener) {
$response = call_user_func($listener['callback'], $data, $event);
// Arrêter la propagation si false retourné
if ($response === false && $propagate) {
$result = false;
break;
}
}
}
// Gestion des wildcards
foreach ($this->wildcards as $pattern => $callbacks) {
if ($this->matchWildcard($pattern, $event)) {
foreach ($callbacks as $callback) {
call_user_func($callback['callback'], $data, $event);
}
}
}
return $result;
}
/**
* Supprimer un listener
*/
public function off($event, $callback = null) {
if ($callback === null) {
// Supprimer tous les listeners de cet événement
unset($this->listeners[$event]);
return;
}
// Supprimer un listener spécifique (complexe...)
if (isset($this->listeners[$event])) {
$this->listeners[$event] = array_filter(
$this->listeners[$event],
function($listener) use ($callback) {
return $listener['callback'] !== $callback;
}
);
}
}
/**
* Vérifier si un pattern wildcard correspond
*/
private function matchWildcard($pattern, $event) {
$regex = str_replace('*', '.*', preg_quote($pattern, '/'));
return preg_match('/^' . $regex . '$/', $event);
}
}
Gestion avancée : priorités et wildcards
Les priorités, c’est crucial ! Plus le nombre est élevé, plus l’événement s’exécute tôt. Ça permet de contrôler finement l’ordre d’exécution :
$events = new EventManager();
// S'exécute en premier (priorité 20)
$events->on('user.login', function($user) {
error_log('Premier : validation sécurité');
}, 20);
// S'exécute en second (priorité 10 par défaut)
$events->on('user.login', function($user) {
error_log('Second : mise à jour session');
});
// Wildcard pour tous les événements user
$events->on('user.*', function($data, $event) {
error_log("Événement user détecté : {$event}");
});
Attention cependant : les wildcards peuvent impacter les performances si vous en abusez ! Je recommande de les utiliser avec parcimonie.
Propagation et contrôle de flux
La propagation, c’est votre filet de sécurité. Un listener peut arrêter toute la chaîne en retournant false :
$events->on('payment.process', function($payment) {
if ($payment['amount'] > 1000) {
// Arrêter le traitement pour validation manuelle
return false;
}
return true;
}, 15);
$events->on('payment.process', function($payment) {
// Ne s'exécute que si le précédent retourne true
process_payment($payment);
});
Bon, je dois avouer que cette approche peut créer des dépendances implicites entre listeners… Il faut documenter clairement ces comportements !
Patterns avancés et gestion asynchrone
Maintenant qu’on a un Event Manager fonctionnel, passons aux choses sérieuses ! Pour gérer des volumes importants d’événements ou des traitements coûteux, on va avoir besoin de patterns plus sophistiqués. L’Observer Pattern nous permettra de découpler nos composants, tandis que les queues d’événements nous offriront la possibilité de traiter certaines actions en arrière-plan.
Observer Pattern et Event Subscribers
L’Observer Pattern, c’est un classique dans le développement ! Au lieu d’enregistrer des callbacks simples, on va créer des classes Observer qui implémentent des méthodes spécifiques. Voici comment structurer ça :
interface EventSubscriberInterface {
public function getSubscribedEvents(): array;
}
class UserRegistrationObserver implements EventSubscriberInterface {
public function getSubscribedEvents(): array {
return [
'user.registered' => 'onUserRegistered',
'user.activated' => 'onUserActivated'
];
}
public function onUserRegistered($user) {
// Envoi email de bienvenue
wp_mail($user->email, 'Bienvenue !', $this->getWelcomeTemplate());
}
public function onUserActivated($user) {
// Log de l'activation
error_log("User {$user->ID} activated");
}
}
Et dans notre Event Manager, on ajoute une méthode pour enregistrer automatiquement ces subscribers :
public function addSubscriber(EventSubscriberInterface $subscriber) {
foreach ($subscriber->getSubscribedEvents() as $event => $method) {
$this->on($event, [$subscriber, $method]);
}
}
L’avantage ? On peut regrouper toute la logique métier dans des classes dédiées. Plus facile à tester et à maintenir !
Implémentation des queues d’événements
Pour les traitements lourds (redimensionnement d’images, envoi d’emails en masse, etc.), on ne peut pas bloquer l’utilisateur. Action Scheduler est parfait pour ça, mais on peut aussi utiliser WP_Queue ou même Redis.
Voici une implémentation avec Action Scheduler :
class AsyncEventManager extends EventManager {
private $async_events = ['image.uploaded', 'newsletter.send'];
public function emit($event, $data = []) {
if (in_array($event, $this->async_events)) {
// Mise en queue asynchrone
as_schedule_single_action(
time(),
'process_async_event',
['event' => $event, 'data' => $data]
);
return;
}
// Traitement synchrone classique
parent::emit($event, $data);
}
}
// Hook pour traiter les événements en queue
add_action('process_async_event', function($args) {
$manager = AsyncEventManager::getInstance();
$manager->processSync($args['event'], $args['data']);
});
Avec Redis (si disponible), on peut faire encore mieux :
private function queueToRedis($event, $data) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$job = json_encode([
'event' => $event,
'data' => $data,
'timestamp' => time()
]);
$redis->lpush('wordpress_events', $job);
}
Gestion des erreurs et fallbacks
Quand on traite des événements de manière asynchrone, les erreurs peuvent passer inaperçues. Il faut prévoir un système robuste :
public function processWithRetry($event, $data, $max_retries = 3) {
for ($attempt = 1; $attempt <= $max_retries; $attempt++) {
try {
$this->emit($event, $data);
return true; // Succès !
} catch (Exception $e) {
error_log("Event {$event} failed (attempt {$attempt}): " . $e->getMessage());
if ($attempt === $max_retries) {
// Dernier essai raté, on stocke pour retry manuel
$this->logFailedEvent($event, $data, $e->getMessage());
return false;
}
// Attente progressive avant retry
sleep(pow(2, $attempt));
}
}
}
private function logFailedEvent($event, $data, $error) {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'failed_events',
[
'event_name' => $event,
'event_data' => serialize($data),
'error_message' => $error,
'created_at' => current_time('mysql')
]
);
}
Pour les fallbacks, on peut également implémenter un système de circuit breaker :
class CircuitBreaker {
private $failure_threshold = 5;
private $timeout = 60; // secondes
public function canExecute($service) {
$failures = get_transient("cb_failures_{$service}") ?: 0;
if ($failures >= $this->failure_threshold) {
return false; // Circuit ouvert
}
return true;
}
public function recordFailure($service) {
$failures = get_transient("cb_failures_{$service}") ?: 0;
set_transient("cb_failures_{$service}", $failures + 1, $this->timeout);
}
}
Performance et optimisations
Quand on commence à avoir beaucoup d’événements, les performances deviennent critiques. Première règle : lazy loading des listeners.
class LazyEventManager extends EventManager {
private $lazy_listeners = [];
public function addLazyListener($event, $class, $method) {
if (!isset($this->lazy_listeners[$event])) {
$this->lazy_listeners[$event] = [];
}
$this->lazy_listeners[$event][] = [
'class' => $class,
'method' => $method,
'loaded' => false
];
}
protected function getListeners($event) {
// Charge les listeners lazy au moment de l'émission
if (isset($this->lazy_listeners[$event])) {
foreach ($this->lazy_listeners[$event] as &$listener) {
if (!$listener['loaded']) {
$instance = new $listener['class']();
$this->on($event, [$instance, $listener['method']]);
$listener['loaded'] = true;
}
}
}
return parent::getListeners($event);
}
}
Pour le caching, on peut stocker les résultats d’événements coûteux :
public function emitCached($event, $data = [], $ttl = 300) {
$cache_key = 'event_' . md5($event . serialize($data));
$cached = get_transient($cache_key);
if ($cached !== false) {
return $cached;
}
$result = $this->emit($event, $data);
set_transient($cache_key, $result, $ttl);
return $result;
}
Et n’oubliez pas de profiler ! Un petit benchmark maison peut vous aider :
public function benchmark($event, $data, $iterations = 100) {
$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
$this->emit($event, $data);
}
$end = microtime(true);
$avg = ($end - $start) / $iterations * 1000;
error_log("Event {$event}: {$avg}ms average over {$iterations} iterations");
}
Avec ces optimisations, vous devriez pouvoir gérer plusieurs milliers d’événements par seconde sans problème !
Cas pratiques : E-commerce et gestion de contenu
Bon, la théorie c’est bien, mais rien ne vaut des exemples concrets ! J’ai eu l’occasion de mettre en pratique ces concepts sur plusieurs projets e-commerce et de gestion de contenu. Voici deux cas d’usage qui m’ont vraiment convaincu de l’efficacité d’un système d’événements custom.
Système de notifications pour WooCommerce
Sur un projet e-commerce récent, j’ai dû implémenter un système de notifications complexe. Le client voulait être alerté pour les commandes, les stocks faibles, les paiements échoués… Le truc, c’est qu’il fallait envoyer ces notifications sur plusieurs canaux : email, SMS, Slack, et même push notifications.
Au début, j’ai voulu faire ça avec les hooks WooCommerce classiques. Résultat ? Un code spaghetti impossible à maintenir. C’est là que j’ai intégré notre EventManager :
class WooCommerceEventHandler {
public function __construct() {
// Integration avec les hooks WooCommerce existants
add_action('woocommerce_new_order', [$this, 'handleNewOrder']);
add_action('woocommerce_payment_complete', [$this, 'handlePaymentComplete']);
add_action('woocommerce_order_status_failed', [$this, 'handlePaymentFailed']);
add_action('woocommerce_low_stock', [$this, 'handleLowStock']);
}
public function handleNewOrder($order_id) {
$order = wc_get_order($order_id);
EventManager::getInstance()->emit('order.created', [
'order_id' => $order_id,
'customer_email' => $order->get_billing_email(),
'total' => $order->get_total(),
'items' => $order->get_items()
]);
}
public function handlePaymentFailed($order_id) {
$order = wc_get_order($order_id);
EventManager::getInstance()->emit('payment.failed', [
'order_id' => $order_id,
'customer_email' => $order->get_billing_email(),
'amount' => $order->get_total(),
'payment_method' => $order->get_payment_method()
]);
}
}
Ensuite, j’ai créé différents listeners pour chaque canal de notification :
class NotificationManager {
public function init() {
$eventManager = EventManager::getInstance();
// Email notifications
$eventManager->on('order.*', [$this, 'sendEmailNotification'], 10);
// SMS notifications (priorité haute pour les paiements échoués)
$eventManager->on('payment.failed', [$this, 'sendSMSNotification'], 5);
// Slack notifications pour l'équipe
$eventManager->on('order.created', [$this, 'notifySlack']);
$eventManager->on('stock.low', [$this, 'notifySlackUrgent'], 1);
}
public function sendEmailNotification($data, $eventName) {
$template = $this->getEmailTemplate($eventName);
wp_mail($data['customer_email'], $template['subject'], $template['body']);
}
public function sendSMSNotification($data) {
// Integration avec une API SMS
$this->smsService->send($data['customer_phone'],
"Problème avec votre commande #{$data['order_id']}");
}
}
Le résultat ? Un code modulaire et facilement extensible. Quand le client a voulu ajouter des notifications Discord, j’ai juste créé un nouveau listener. Fini le bricolage dans les hooks existants !
Workflow de publication automatisé
L’autre cas pratique qui m’a marqué, c’est un site d’actualités avec un workflow de publication complexe. Chaque article devait passer par plusieurs étapes : validation du contenu, génération de thumbnails, optimisation SEO, puis syndication sur les réseaux sociaux.
Avant, tout ça était géré avec des fonctions appelées directement dans wp_insert_post. C’était l’enfer pour déboguer ! Voici comment j’ai restructuré ça avec notre système d’événements :
class PublicationWorkflow {
public function __construct() {
// Hooking sur les événements WordPress existants
add_action('transition_post_status', [$this, 'handlePostStatusChange'], 10, 3);
add_action('wp_insert_post', [$this, 'handlePostInsert'], 10, 2);
}
public function handlePostStatusChange($new_status, $old_status, $post) {
if ($new_status === 'publish' && $old_status !== 'publish') {
EventManager::getInstance()->emit('post.published', [
'post_id' => $post->ID,
'post_title' => $post->post_title,
'post_content' => $post->post_content,
'author_id' => $post->post_author
]);
}
}
}
class ContentProcessor {
public function init() {
$eventManager = EventManager::getInstance();
// Workflow orchestré avec les priorités
$eventManager->on('post.published', [$this, 'validateContent'], 100);
$eventManager->on('post.validated', [$this, 'generateThumbnails'], 90);
$eventManager->on('thumbnails.generated', [$this, 'optimizeSEO'], 80);
$eventManager->on('seo.optimized', [$this, 'scheduleSocialSharing'], 70);
}
public function validateContent($data) {
// Validation du contenu (longueur, mots-clés, etc.)
if ($this->isContentValid($data)) {
EventManager::getInstance()->emit('post.validated', $data);
} else {
EventManager::getInstance()->emit('post.validation_failed', $data);
}
}
public function generateThumbnails($data) {
// Génération des différentes tailles d'images
$thumbnails = $this->createThumbnails($data['post_id']);
$data['thumbnails'] = $thumbnails;
EventManager::getInstance()->emit('thumbnails.generated', $data);
}
public function scheduleSocialSharing($data) {
// Programmation du partage sur les réseaux sociaux
wp_schedule_single_event(time() + 300, 'social_sharing_event', [$data]);
EventManager::getInstance()->emit('social.scheduled', $data);
}
}
Ce qui est génial avec cette approche, c’est que chaque étape peut échouer indépendamment. Si la génération de thumbnails plante, ça n’empêche pas l’optimisation SEO de se faire. Et surtout, on peut facilement ajouter des étapes au workflow sans casser l’existant.
Personnellement, ces deux exemples m’ont vraiment ouvert les yeux sur la puissance d’un système d’événements bien conçu. Plus jamais je ne ferais du e-commerce ou de la gestion de contenu complexe sans cette architecture !
