Événements envoyés par le serveur


1Présentation

Cette page documente la fonctionnalité de Temma relative à la norme server-sent events, qui permet de faire une communication unidirectionnelle (du serveur vers le navigateur) en temps réel.

Attention, si vous cherchez la documentation relative à la gestion des événements asynchrones, regardez du côté des sources de données ZeroMQ, SQS et Beanstalk.

Lorsqu'un code Javascript se connecte à une URL pour recevoir des SSE (server-sent events), la connexion reste ouverte, un peu à la manière d'une websocket. Si la connexion venait à être coupée, le client s'y reconnectera automatiquement.

Du côté de Temma, un nouveau type de contrôleurs apparaît, les contrôleurs d'événements, qui dérivent de l'objet de base \Temma\Web\EventController. Ces contrôleurs ne peuvent servir qu'à traiter les SSE, et ont quelques différences par rapport aux contrôleurs classiques (qui dérivent de \Temma\Web\Controller) :

  • Ils sont prévus pour s'exécuter aussi longtemps que nécessaire.
  • Ils n'exécutent pas de vue à la fin de leur exécution, car ils envoient des messages d'événement au cours de leur exécution.
  • Pas de vue = pas de template = pas de variables de template.
  • Lorsqu'une valeur est définie (à la manière dont sont habituellement définies les variables de template), elle est immédiatement envoyée au client, qui la recevra sous forme d'événement.

Attention, un grand nombre de connexions peuvent rester ouvertes (autant que de client connectés au site). Vérifiez que votre serveur accepte un nombre suffisant de connexions simultanées.


2Exemple

Imaginons une page web très simple. Elle comporte un code Javascript qui va se connecter à l'URL /message/fetch, et affichera les événements du canal "demo" dans une liste.

Voici le code HTML de la page :

<html>
<head>
    <!-- chargement du code Javascript -->
    <script src="event-manager.js"></script>
</head>
<body>
    <!-- liste qui va contenir les messages d'événements -->
    <ul id="liste">
    </ul>
</body>
</html>

Et le code Javascript (fichier event-manager.js) :

// connexion au flux d'événements
const evtSource = new EventSource("/message/fetch");

// traitement des événements entrants sur le canal "demo"
evtSource.addEventListener("demo", function(event) {
    // récupération du texte (sérialisé en JSON dans l'événement)
    const str = JSON.parse(event.data);

    // création du nouvel élément de liste
    const newElement = document.createElement("li");
    // ajout du texte de l'événement comme contenu de l'élément de liste
    newElement.textContent = str;

    // ajout de l'élément à la liste (dans la page HTML)
    document.getElementById("liste").appendChild(newElement);
});

Voici le code du contrôleur qui va envoyer les événements (il envoie un message textuel toutes les 2 secondes) :

// contrôleur Message
class Message extends \Temma\Web\EventController {
    // action fetch
    public function fetch() {
        $i = 1;
        // boucle infinie
        while (true) {
            // envoi d'un événement sur le canal "demo"
            $this['demo'] = "Message n°$i";
            // incrémentation du compteur
            $i++;
            // attente de 2 secondes avant l'envoi du prochain message
            sleep(2);
        }
    }
}

Toutes les deux secondes, le contrôleur va envoyer un événement, et le client va ajouter le message de cet événement à la liste présente dans la page web.


3Principes des contrôleurs d'événement

En dehors des spécificités listées plus haut (pas de template ni de vue), les contrôleurs d'événements fonctionnent globalement comme des contrôleurs classiques. D'ailleurs, l'objet \Temma\Web\EventController hérite de \Temma\Web\Controller.

Ainsi, les concepts d'action racine, d'action proxy et d'action par défaut restent identiques ; pareil pour l'initialisation et la finalisation des contrôleurs (voir la documentation sur les contrôleurs).

Les plugins fonctionnent aussi de la même manière. Prenez toutefois en compte que les post-plugins ne seront pas traités à chaque envoi d'événement, mais uniquement à la fin de l'exécution (sachant que l'exécution peut être interrompue par le serveur à cause d'un temps d'exécution trop long).


4Envoi d'événements

Comme vu dans l'exemple plus haut, pour envoyer un événement, il suffit de définir une valeur de la même manière qu'on définit habituellement une variable de template, en utilisant une écriture de type tableau associatif. La clé est le nom du canal d'événement, et la valeur peut contenir n'importe quelle donnée PHP (qui sera sérialisée en JSON).

Exemples :

// envoi de deux messages textuels sur le canal "msg"
$this['msg'] = "Premier message";
$this['msg'] = "Second message";

// envoi d'une liste d'éléments sur le canal "update"
$this['update'] = [
    123 => [
        'id'     => 123,
        'name'   => "Projet Neptune",
        'author' => "Anne Honyme",
    ],
    456 => [
        'id'     => 456,
        'name'   => "Projet Pluton",
        'author' => "Alain Connu",
    ],
];

À chaque fois qu'une donnée est envoyée, le contrôleur regarde d'abord automatiquement si la connection avec le client est toujours ouverte. Si ce n'est pas le cas, une exception \Temma\Exceptions\FlowQuit est levée ; si elle n'est pas interceptée, cela conduira à l'arrêt des traitements (voir la documentation du flux d'exécution).

Si le message a pu être envoyé, le temps maximal d'exécution du script est repoussé. La valeur utilisée est celle configurée pour la directive max_execution_time du fichier php.ini. Si cette valeur n'est pas configurée, le temps maximal d'exécution est défini à 30 secondes. Si cette valeur est configurée à zéro (durée illimitée), elle n'est pas modifiée.


5Gestion des canaux d'événements

Il est possible de savoir si des messages ont été envoyés sur un canal, et si oui combien.

Exemples :

// est-ce qu'un canal a déjà été utilisé ?
if (isset($this['monCanal']))
    print("Le canal 'monCanal' a été utilisé.");

// récupération du nombre d'événements envoyés sur un canal
$nbr = $this['monCanal'];

// réinitialisation d'un canal
// (faire comme s'il n'avait jamais été utilisé)
unset($this['monCanal']);

Les contrôleurs d'événements proposent des méthodes spécifiques utilisables dans le code des actions.

$this->_checkConnection()
Vérifie que la connexion avec le client est toujours ouverte. Si ce n'est pas le cas, une exception \Temma\Exceptions\FlowQuit est levée (voir la documentation de flux d'exécution).

$this->_renewTimeLimit()
Cette méthode commence par appeler la méthode _checkConnection(), puis elle repousse le temps maximal d'exécution du script. La valeur utilisée est celle configurée pour la directive max_execution_time du fichier php.ini. Si cette valeur n'est pas configurée, le temps maximal d'exécution est défini à 30 secondes. Si cette valeur est configurée à zéro (durée illimitée), elle n'est pas modifiée.

$this->_ping()
Envoi un événement sur le canal ping, qui contient un tableau associatif dont la clé time contient la date et heure actuelle au format ISO 8601.