Notifications temps réel via Symfony2 + NodeJs + Redis

Dernièrement, j’ai développé une super appli PHP Symfony2. Une feature de dernière minute est apparue comme par magie dans la tête du client (NON CA N’ARRIVE JAMAIS) et c’était des notifications en temps réel pour les utilisateurs.

J’ai hésité à faire directement de l’ajax polling, puis j’ai découvert une solution bien plus sexy.

tumblr_m3wo9r9Jjc1rrlbpeo1_250

Utiliser un adaptateur SocketIO pour PHP, un serveur NodeJS et Redis !

Bref, aujourd’hui on va apprendre à rajouter dans votre appli Symfony existante des interactions temps réel.

Bon déjà, comment ça marche ?

D’abord, il faut que je vous explique chacune des technos utilisées.

NodeJS: A part si vous avez vécu dans un trou ces 5 dernières années, vous en avez déjà entendu parler. Il s’agit d’une plate-forme côté serveur faite en JavaScript. NodeJS c’est un ensemble de librairies, un environnement d’exécution mais aussi une machine virtuelle ! NodeJs se distingue par son aspect asynchrone (ça été fait pour) et sa forte résistance aux montées en charge.

Redis: Une base de données clef-valeur avec la particularité intéressante que tout est stocké dans la RAM.
Autant vous dire que niveau performance ça envoie violemment du steak. Il y a une partie événement intéressante dans Redis :  le système SUBSCRIBE/PUBLISH.
Subscribe peut écouter un canal de communication et Publish peut envoyer un message dans un canal. On va s’en servir ici !

SocketIO: Il s’agit d’une bibliothèque JavaScript pour les applications web qui permet le temps réel, elle comporte deux parties : une côté client qui fonctionne dans le navigateur et une côté serveur pour Node.js.
Elle utilise le protocole WebSocket mais avec un fallback en Ajax Pooling. Ce dernier détail fait fonctionner SocketIO même sur l’ordi de mamie avec son super IE 5.5 (rien que ça).
Ici on utilisera également un adaptateur PHP de socket IO !

Le plan c’est qu’on va installer l’adaptateur Socket IO en PHP coté Symfony2.
Il nous permettra d’envoyer un signal (emit) quand on le souhaitera dans notre controller au serveur NodeJS et à Redis.
Ces derniers seront en écoute permanente d’un signal, une fois reçu ils enverront également un signal à tous les clients connectés en temps réel.
EASY ! Allez go on se lance.

Le roi et ses serviteurs

throne

Commençons par NodeJS et Redis!
Je pars du principe que vous êtes sur une distrib’ linux debian/ubuntu.
Vous n’êtes pas sur Linux ? Sérieusement ? Bon bah vous pouvez utiliser ce fabuleux outil pour l’install alors, l’installation est simple sur toutes les plateformes.

Mais c’est encore plus simple dans la console de votre Linux :

apt-get install nodejs redis-server php5-redis npm

Oui on installe NPM avec car on va avoir besoin de quelques modules NodeJS pour faire fonctionner notre solution.
D’ailleurs n’hésiter pas à redémarrer votre PHP, notamment pour la prise en compte de l’extension Redis.

N’oubliez pas de lancer aussi votre serveur redis, sans quoi vous allez avoir de la belle erreur de connexion!

redis-server

Maintenant faites vous un petit dossier dans un coin, quelque part sur votre serveur, et installons ces modules à cet endroit.

npm install socket.io socket.io-redis express redis

On installe donc socket.io côté serveur pour NodeJS, Express qui est notre framework web coté Node et enfin Redis.

Une fois que tout est installé on va créer notre serveur JS !
Pour se faire, créons un fichier “app.js” dans votre dossier qui ressemble à ça :

/path/to/server/app.js

'use strict';

// Getting server, socket and redis
var app = require('express')();
var http = require('http').Server(app);
var server = require('http').createServer();
var io = require('socket.io').listen(server);
var redis = require('socket.io-redis');

// Adapter SocketIO - Redis
io.adapter(redis({ host: '127.0.0.1', port: 6379 }));

// On socket connexion
io.on('connection', function(socket){
    // Listening on notification
    socket.on('notification', function(msg){
        // Emit
        io.emit('notification', msg);
    });
});

server.listen(8081, '127.0.0.1');

Enfin vous pouvez lancer votre serveur :

nodejs app.js

Rien ne se passe, oui, c’est normal, votre serveur tourne et attend des news du channel de notifications.
Retour sur notre projet Symfony 2!

Le FUN pour tous en live !

fun-live

On va commencer par installer notre adaptateur socket IO PHP et on va utiliser celui là via composer.
Allez a la racine de votre projet et lancer l’installation :

php composer.phar require rase/socket.io-emitter

Comme vous pouvez le constater, il s’agit d’une librairie et pas d’un bundle, donc elle ne sera pas automatiquement chargée !
Pas de problème on va la rajouter manuellement.
Rendez-vous dans votre fichier autoload, on va insérer notre namespace de la façon suivante :

/app/autoload.php

...

$loader->add('SocketIO\Emitter', __DIR__.'/../vendor/rase/socket.io-emitter/src/');

return $loader;

Désormais le namespace “SocketIO\Emitter” pointera vers notre librairie installer dans le vendor grâce à composer. On pourra l’utiliser à tout moment dans notre controller !

Allez hop sans plus attendre direction ce dernier :

/src/Acme/AcmeBundle/Controller/DefaultController.php

<?php
namespace Acme\AcmeBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

use SocketIO\Emitter;

class DefaultController extends Controller
{

    public function indexAction(Request $request)
    {
        $notification = 'A message has been received by the server!<br />';

        // Handling form	
        $form = $this->createFormBuilder()->getForm();
        $form->handleRequest($request);

        // If form valid
        if ($form->isValid()) {

            // Calling PHP Redis from global namespace
            $redis = new \Redis();

            // Connecting on localhost and port 6379
            $redis->connect('127.0.0.1', '6379');

            // Creating Emitter
            $emitter = new Emitter($redis);

            // Emit a notification on channel 'notification'
            $emitter->emit('notification', $notification);

            // Returning status via JsonResponse
            $response = new JsonResponse();
            $response->setData(array(
                'notification' => $notification
            ));

            return $response;

        }

        return $this->render('AcmeAcmeBundle:Default:index.html.twig', array(
            'form' => $form->createView()
        ));
    }

}

Alors concernant le controller:

Dans la première partie on a juste créé un formulaire vide, on va l’utiliser pour déclencher notre process d’emit.
Une fois le formulaire validé, on instancie Redis, on s’y connecte, on crée un emiter et on envoie un message sur le channel notification.
On renvoie enfin au JSON ce qu’on a envoyé au front, pour avoir l’info que tout s’est bien passé.

Si le formulaire n’a pas été validé (première entrée sur la page) on renvoie le template suivant :

/src/Acme/AcmeBundle/Resources/views/index.html.twig

<!DOCTYPE html>
<html lang="en">
    
    <head>
        <meta charset="UTF-8">
    </head>
    
    <body>
 
        {{ form_start(form, {'attr': {'class': 'form'}}) }}
            {{ form_widget(form) }}
            <input type="submit" />
        {{ form_end(form) }}
  
  <br />
  
  <div class="client">
  </div>
  
  <br />
 
  <div class="server">
  </div>
 
        {% block javascripts %}
            <script src="https://cdn.socket.io/socket.io-1.2.0.js"></script>
            <script src="https://code.jquery.com/jquery-1.11.1.js"></script>
            <script>
                $(document).ready(function() {
    
                    // Getting socket
                    var socket = io('http://127.0.0.1:8081');

                    // Listening on notification from server
                    socket.on('notification', function (data) {
                        $('.server').append(data);
                    });

                    // Listener on form submit event
                    $(document).on('submit', '.form', function(e) {
                        e.preventDefault();

                        $.ajax({
                            type: 'POST',
                            url: '/',
                            data: $(this).serialize(),
                            success : function(response){
                                $('.client').append('A message has been sent to server from this client!<br />');
                            },
                            error : function(response){
                                console.log('Something went wrong.');
                            },
                            cache: false
                        });

                        return false;
                    });

                });
            </script>
        {% endblock %}
    </body>
 
</html>

Dans ce template on commence très simplement par afficher un formulaire vide.
La partie intéressante est dans le block javascript. En effet, on appelle la librairie de socket IO (ici directement sur le cloud, à vous de voir) ainsi que jQuery.

Ensuite on se connecte au socket sur notre serveur Node.
On écoute le channel notification, à la réception d’un message sur ce channel de la part du serveur on ajoute une ligne dans la div serveur.

Enfin, on écoute la soumission du formulaire et ont le POST en AJAX. Si tout ça se passe bien on indique dans la div client qu’ons as bien envoyer le message.

Voilà, ouvrez deux fenêtres de navigateur, et commencer à valider tour à tour les formulaires dans chacune des fenêtres, vous allez voir que tout se fait en temps réel dans chacunes des fenêtres !

dealwithit

Epilogue

Je n’en parle pas mais je vous conseille d’aller voir la documentation de l’adaptateur socket, en effet vous y trouverez les notions  de broadcasting qui peuvent être très utiles dans beaucoup de cas!
Enfin sachez qu’il existe des bundles (implémenter via une librairie) comme Elephant.io qui peuvent vous aider aussi bien que ma solution !

Qui me parle ?

Je suis un dev. En ce moment je suis Backend Développeur / DevOps à Ubisoft. Je suis passionné du dev et expert en blagues. Je continue à te parler quotidiennement sur mon Twitter. Tu peux m'insulter à cette e-mail ou le faire directement dans les commentaires juste en dessous.

Commentaire(s)

  1. Bonjour super tuto cependant j’ai un souci :
    Attempted to load class “Redis” from the global namespace.
    Did you forget a “use” statement?
    j’ai essayé de rajouter user \Redis; mais rien y fait, un petit coup de main ne serait pas de refus.
    Merci !

    1. Bonjour,

      Apparemment vous avez un problème avec l’extension Redis pour PHP

      vérifié tout ces points :
      – installation serveur redis et extension redis pour PHP => apt-get install redis-server php5-redis
      – redémarrage du serveur apache => /etc/init.d/apache2 reload
      – démarrage du serveur redis => redis-server

      Enlever le : use \Redis.
      Le \ sert à utiliser l’espace de nom global pour appeler Redis.

    2. Bonjour,

      Apparemment vous avez un problème avec l’extension Redis pour PHP

      vérifié tout ces points :
      – installation serveur redis et extension redis pour PHP => apt-get install redis-server php5-redis
      – redémarrage du serveur apache => /etc/init.d/apache2 reload
      – démarrage du serveur redis => redis-server

      Enlever le : use Redis.
      Le sert à utiliser l’espace de nom global pour appeler Redis.

      1. J’ai le même le problème, pourtant je vérifié l’installation, redis est bien intallé, le serv run parfaitement, php-redis installé. Une diée ?

  2. Très intéressant ! Ça donne envie de jouer avec 🙂
    Juste deux petites coquilles qui m’ont fait tiquer :
    – On ne parle pas d’ajax pooling (regroupement / mutualisation de ressources) mais de polling (interrogation)
    – “A message has been received” (participe passé)

    1. Bonjour et merci pour vos remarques !

      L’idée de cette article et de montrer comment on peut faire fonctionner très simplement des notifications en temps réel. En gros présenter les outils et comment les faire communiquer.

      Après à chacun d’adapter à son besoin et aux contraintes de sécurité 😉

  3. Approche intéressante, cependant grosse coquille: toute la partie qui écoute les événements dans app.js (l14-20) est inutile ! le but de ce genre d’approche est que “l’adapter” côté serveur JS se charge de faire le lien entre le serveur web (ici PHP) et le client en lui-même.

  4. Bonsoir,
    Je rencontre actuellement un soucis avec la parti var socket = io(‘http://127.0.0.1:8081’);
    Quand je suis en local sur mon environnement de dev aucun problème cela fonctionne correctement
    Par contre dès que je passe sur le serveur de production j’ai différent cas de figure :
    Si je laisse : var socket = io(‘http://127.0.0.1:8081’) ou var socket = io(‘http://localhost:8081’) j’ai une erreur dans la console Chrome qui me dit net::ERR_CONNECTION_REFUSED . Le seul moyen pour que l’erreur disparaisse c’est que je lance en local sur ma machine redis et node.

    Si j’utilise var socket = io(‘http://domaine.com:8081’) j’ai une erreur net::ERR_CONNECTION_TIMED_OUT
    pour l’url : http://domaine.com:8081/socket.io/?EIO=3&transport=polling&t=1454540703012-5

    Sur le net j’ai vu que beaucoup préconise d’utiliser var socket = io() mais là je vois une 404 car socket.io tente de se connecter à http://domaine.com/socket.io/?EIO=3&transport=polling&t=1454540703012-5

    Est ce que quelqu’un aurait une piste sachant que j’ai suivis ce tuto du début à la fin.

    Merci d’avance

    1. Bonjour,

      Je me permet de relancer ce post car après plusieurs tentative je me retrouve confronté au même problème que cité plus haut.
      Est ce que la solution a été trouvé ?

      D’avance merci pour votre retour.

      1. Non par faute de temps j’ai du passer par un ajax jquery exécuter à interval régulier.
        Je pense que la solution se situe au niveau du localhost ou il faut mettre ton domaine (domaine.com) et sans doute ouvrir les ports sur ton serveur

  5. bonjour,
    J’ai suivi votre tuto et ça marche très bien cependant j’ai un comportement étrange. J’i voulu ajouter un console.log dans la partie app.js :

    socket.on(‘notification’, function(msg){

    console.log(‘test’);

    io.emit(‘notification’, msg);

    });
    Mais ça n’a pas marché. Du coup je suis allé plus en supprimant toute la partie “emit” du fichier app.js. mon fichier app;js ressemble donc plus qu’à ça :

    ‘use strict’;
    var app = require(‘express’)();
    var http = require(‘http’).Server(app);
    var server = require(‘http’).createServer();
    var io = require(‘socket.io’).listen(server);
    var redis = require(‘socket.io-redis’);
    io.adapter(redis({ host: ‘127.0.0.1’, port: 6379 }));
    server.listen(999, ‘monnomdedomaine.com’);

    Rien d’autre! et ça fonctionne toujours malgré le reémarrage de node, de redis de php-FPM de nginx (même si ça n’a rien à voir g même vidé le cache sf).

    Ca rejoins donc ce que disait Blyex il y a 8mois… toute cette partie est inutile !!!

    Autre chose: J’ai du mettre mon nom de domaine plutôt que 127.0.0.1 ici:
    server.listen(999, ‘monnomdedomaine.com’);

  6. Bonjour à tous.
    D’abord merci pour cette excellent tuto et merci pour son retour à @disqus_K8YfdfW3vG:disqus .
    J’ai pu avancer et ne retrouve aucune erreur “visible” mais une fois que je valide le formulaire, j’ai bien le retour json mais le emit de SocketIoEmitter ne retourne rien au serveur node.
    Du coup je n’ai aucun event notification retourné.

    Savez vous d’où peux venir le soucis ?

    D’avance merci pour votre retour.

    Lotfi

    1. Bonjour @lotfiberrahal:disqus,
      je me retrouve actuellement dans la même situation… et j’avoue désespérer un peu.
      Avez-vous trouver une solution?

      Mathias

      1. Bonjour @mathias_martin:disqus donne moi ton skype on parlera sans polluer ici sachant que je n’ai pas trouvé de solution concrète mais une alternance qui n’est pas traité ici et ça n’apportera rien à la discussion.

T'en penses quoi ?

Your email address will not be published. Required fields are marked *