new-york

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 :

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!

 

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

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

Enfin vous pouvez lancer votre serveur :

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 :

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

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

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

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 !

Categories: NodeJS, PHP, Symfony2

  • http://local zaz

    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 !

  • Vooodoo

    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.

  • Djuuu

    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é)

  • Vooodoo

    Merci pour les coquilles ! C’est corrigé !

  • Vooodoo

    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.

  • YunYun

    Pas de contrôle d’authentification sur le serveur node ? Pourquoi ne pas fonctionner avec les event listener de Symfony 2 ?

  • Vooodoo

    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é 😉

  • Blyex

    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.

  • Nicolas Roux

    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

  • zarloon

    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’);

  • Pierre Dommerc

    tuto intéressant, merci !

  • Pierre Dommerc

    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 ?