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 !

Et sinon tu peut me follow sur twitter, m'insulter à cette e-mail ou le faire directement dans les commentaires juste en dessous.