# Cron dupliqué à chaque page : le piège de wp_schedule_event

> Une tâche censée s'exécuter une fois par jour finit par tourner cinquante fois en simultané. Le symptôme est classique, le correctif tient en trois lignes.

- Auteur : Clément Hadrot
- Publié le : 2024-01-24
- Mis à jour le : 2024-01-24
- Catégorie : Extensions
- URL : https://wpmoderne.dev.wordpress-developpement.fr/extensions/cron-duplique-chaque-page-piege-wp-schedule-event/

## L’essentiel

- wp_schedule_event sans garde-fou empile des événements identiques
- wp_next_scheduled est la vérification qui manque presque toujours
- wp-cron.php se déclenche à chaque visite, pas selon un vrai calendrier

Un client nous a contactés parce que son site d'annonces immobilières envoyait le même e-mail récapitulatif à ses agents jusqu'à quarante fois par jour, au lieu d'une seule fois comme prévu. Aucune erreur dans les journaux, aucun plugin de spam suspect. La cause était plus banale : une extension maison programmait une tâche cron à chaque chargement d'une page d'administration précise, sans jamais vérifier si cette tâche existait déjà.

Ce bug se répète d'un projet à l'autre depuis des années, tant l'erreur est facile à commettre. Elle mérite qu'on s'y attarde une bonne fois, avec le correctif exact et les raisons pour lesquelles WP-Cron se comporte ainsi par nature.

## Symptôme : des événements qui s'accumulent

La commande suivante, exécutée sur le site du client, a révélé l'ampleur du problème :

```
wp cron event list --fields=hook,next_run_relative,schedule \
  --format=table | grep envoi_recap_agents
```

Le résultat affichait quarante-sept lignes identiques pour le hook `envoi_recap_agents`, chacune programmée à quelques secondes d'intervalle les unes des autres. WordPress ne fusionne jamais deux événements cron portant le même nom si leurs arguments diffèrent, même légèrement, et sur ce site, un identifiant de session généré à chaque appel s'était glissé par erreur dans les arguments de la tâche.

## Diagnostic : l'appel fautif

Le code incriminé ressemblait à ceci, placé directement dans le rendu d'un écran d'administration, donc exécuté à chaque chargement de cette page par n'importe quel agent connecté :

```
function afficher_ecran_agents() {
    // ... rendu de l'écran ...

    wp_schedule_event( time(), 'daily', 'envoi_recap_agents', array( uniqid() ) );
}
add_action( 'admin_init', 'afficher_ecran_agents' );
```

Deux fautes se cumulent ici. D'abord, l'appel est placé dans `admin_init`, donc rejoué à chaque affichage de la page, sans aucune condition. Ensuite, l'argument `uniqid()` change à chaque appel, ce qui empêche WordPress de reconnaître qu'une tâche identique existe déjà, puisque la comparaison porte sur le hook et sur les arguments combinés.

## Le correctif : wp_next_scheduled avant toute programmation

La règle à retenir est simple et devrait précéder systématiquement tout appel à `wp_schedule_event()` : vérifier d'abord qu'aucun événement identique n'est déjà programmé, avec des arguments strictement identiques.

> L'essentiel à retenir : wp_schedule_event sans garde-fou empile des événements identiques ; wp_next_scheduled est la vérification qui manque presque toujours ; wp-cron.php se déclenche à chaque visite, pas selon un vrai calendrier

```
function afficher_ecran_agents() {
    // ... rendu de l'écran ...

    if ( ! wp_next_scheduled( 'envoi_recap_agents' ) ) {
        wp_schedule_event( time(), 'daily', 'envoi_recap_agents' );
    }
}
add_action( 'admin_init', 'afficher_ecran_agents' );
```

Deux changements corrigent le problème : la suppression de l'argument variable inutile, et la vérification via `wp_next_scheduled()`, qui recherche un événement portant exactement le même hook et les mêmes arguments. Sans argument du tout, cette vérification devient fiable et stable dans le temps.

### Pourquoi ne pas programmer à l'activation seulement ?

La meilleure pratique reste de programmer la tâche une seule fois, lors de l'activation de l'extension via `register_activation_hook()`, et de la déprogrammer proprement à la désactivation :

```
register_activation_hook( __FILE__, function() {
    if ( ! wp_next_scheduled( 'envoi_recap_agents' ) ) {
        wp_schedule_event( time(), 'daily', 'envoi_recap_agents' );
    }
} );

register_deactivation_hook( __FILE__, function() {
    $timestamp = wp_next_scheduled( 'envoi_recap_agents' );
    if ( $timestamp ) {
        wp_unschedule_event( $timestamp, 'envoi_recap_agents' );
    }
} );
```

Cette approche évite complètement le besoin de vérifier à chaque chargement de page : la tâche est programmée une fois pour toutes, et le hook `wp_unschedule_event()` nettoie proprement lors de la désactivation, ce qui évite aussi les tâches fantômes qui continuent de tourner après la suppression d'une extension.

## Nettoyer les tâches déjà accumulées

Sur un site déjà touché, corriger le code ne suffit pas : les événements déjà programmés restent en base tant qu'on ne les supprime pas explicitement. La commande WP-CLI suivante supprime toutes les occurrences d'un hook donné :

```
wp cron event delete envoi_recap_agents
```

Il faut la répéter jusqu'à ce que `wp cron event list` ne renvoie plus aucune ligne pour ce hook, car chaque appel ne supprime qu'une seule occurrence à la fois lorsque les arguments diffèrent d'un événement à l'autre.

- Toujours encadrer `wp_schedule_event()` d'un test `wp_next_scheduled()`, sans exception, même pour un prototype rapide.
- Programmer la tâche à l'activation de l'extension plutôt que dans un hook rejoué à chaque page.
- Éviter les arguments variables dans les tâches récurrentes, sauf besoin réel et documenté, car ils cassent la détection de doublon.

> Un rappel qui vaut pour toute extension : WP-Cron n'est pas un vrai ordonnanceur système, c'est un mécanisme déclenché par le trafic du site. Une tâche mal gérée ne se contente pas de ne pas s'exécuter à l'heure prévue, elle peut s'exécuter bien trop souvent.

## Prévention

Ce type de bug se détecte tôt avec une simple habitude de revue de code : chercher systématiquement les occurrences de `wp_schedule_event` dans une extension et vérifier qu'aucune ne s'exécute en dehors d'un hook d'activation ou d'une garde explicite. Un test automatisé simple, qui appelle deux fois de suite la fonction d'enregistrement et vérifie qu'un seul événement existe ensuite, suffit à éviter que ce piège ne revienne dans une future version de l'extension.
