Il y a de plus en plus d’applications web qui utilisent le Websocket permettant d’afficher les informations au utilisateurs en temps réel. Etant l’une des frameworks PHP les plus en vogue actuellement, Laravel offre avec “Laravel Echo”, un système de “broadcasting d’évènement” qui donne la possibilité, coté client, de facilement se souscrire à des évènements qui sont diffusés en arrière plan via des canaux ou channels par le serveur. Le coté client réagit vis-à-vis de ces évènements sans que celui-ci ait besoin de rafraichir la page. Cet article va vous guider pas à pas à utiliser Laravel Echo.

Base

Créer un projet laravel

1composer create-project --prefer-dist laravel/laravel echo-project

Configuration

1cd echo-project
2npm install

On va avoir besoin du service de broadcasting de Laravel, il faut donc activer le service provider correspondant dans /config/app.php en décommentant cette ligne:

1<?php
2
3App\Providers\BroadcastServiceProvider::class
4

Cette classe va ajouter les routes nécessaires au broadcasting d’évènements et ensuite charger les routes qui se trouvent dans le fichier routes/channels.php

Après cela, on aura une nouvelle route broadcasting/auth que l’on peut constater avec la commande:

1php artisan route:list

Cette route est responsable de la gestion d’authentification des “private channels” que l’on verra plus en détails.

Dans channels.php, on peut voir ces lignes de code:

1<?php
2
3Broadcast::channel('App.User.{id}', function ($user, $id) {
4    return (int) $user->id === (int) $id;
5});
6

C’est l’authorization qui détermine si l’utilisateur courant $user est authorisé à écouter sur le channel App.User.{id}

Un channel est une sorte de point de diffussion du serveur avec lequel le client peut se brancher pour y écouter des évènements.

Pour chaque channel, on peut spécifier si tel ou tel utilisateur a le droit de s’y connecter.

Dans le fichier config/broadcasting.php, on a les configurations du broadcasting de Laravel. Laravel support par defaut les drivers suivants: pusher, redis, log, null

Le driver log nous permet de diffuser un évènement dans le fichier log de Laravel, utiliser surtout à des fins de test et de déboggage.

Mais nous allons utiliser pusher parce que pusher propose un service gratuit et c’est surtout le plus facile à utiliser.

Dans le fichier .env, modifier le BROADCAST_DRIVER=log en BROADCAST_DRIVER=pusher.

Dans la configuration de connexion de pusher config/broadcasting.php, n’oublier pas de mettre encrypted à false pour que Pusher accepte aussi les requêtes http et pas seulement des https. (A noter que ceci est seulement pour les environnements de développement et n’est surtout pas recommandé en production).

On doit créer un compte sur dashboard.pusher.com et créer notre premier application.

Après la création du compte, on arrive à l’ecran de génération d’application pusher, il suffit de nommer notre app et cliquer sur “Create my app”

Ensuite, on est redirigé sur le dashboard de notre app. Sur ce dashboard on a un onglet App Keys qui contient la configuration de notre app.

Dans notre projet Laravel, on doit installer les dépendances de pusher

1composer require pusher/pusher-php-server

et y renseigner les configurations suivantes (dans le .env):

1PUSHER_APP_ID=563433                    #app_id
2PUSHER_APP_KEY=65e1feabbe881dc01561     #key
3PUSHER_APP_SECRET=69312615230912d19f4a  #secret
4PUSHER_APP_CLUSTER=mt1                  #cluster

Pour terminer la configuration, il faut installer les diffuseurs javascript coté client, notamment laravel-echo et pusher-js.

1npm install laravel-echo pusher-js

Utilisation

Diffusion du message du coté Serveur / Pusher

On aura besoin d’un évènement Laravel à diffuser. Créons un évènement StatusCommandeMisAjour qui va, soit disant, s’exécuter lorsque le status d’une commande a été mis à jour.

1php artisan make:event StatusCommandeMisAjour

Par défaut, l’évènement StatusCommandeMisAjour est un évènement standard coté serveur avec lequel on peut brancher plusieurs listeners.

Pour en faire un évènement diffusable coté client, il faut que celui-ci implémente l’interface “ShouldBroadcast” déjà importer avec:

1<?php
2
3use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
4

La declararion de la classe StatusCommandeMisAjour deviendra donc :

1<?php
2
3class StatusCommandeMisAjour implements ShouldBroadcast
4

Par défaut, on a la fonction broadcastOn qui retourne un PrivateChannel nommé “channel-name”.

Un “PrivateChannel” (Illuminate\Broadcasting\PrivateChannel) est un channel qui ne peut être écouté que par des utilisateurs authentifiés et authorisés.

On a d’autres types de channels qui sont:

  • Un “Channel” (Illuminate\Broadcasting\Channel), c’est le channel par défaut ou public channel qui peut être écouter par plusieurs utilisateurs que ceux soient visiteurs ou authentifiés.
  • Un “PresenceChannel” (Illuminate\Broadcasting\PresenceChannel), c’est un private channel avec la possibilité d’identification de l’utilisateur connecté.

Pour commencer, nous allons utiliser le public channel. Dans la fonction broadcastOn, on aura:

1<?php
2
3public function broadcastOn()
4{
5    return new Channel('commandes');
6}
7

On va créer une simple classe “Command” dans le repertoire app:

 1<?php
 2
 3class Command
 4{
 5    public $id;
 6
 7    public function __construct($id)
 8    {
 9        $this->id = $id;
10    }
11}
12

On va supposer que la route update fait la mise à jour d’une commande et exécute l’évènement StatusCommandeMisAjour.

Dans routes/web.php, ajouter:

1<?php
2
3Route::get('/update', function () {
4    $command = new Command(1);
5
6    StatusCommandeMisAjour::dispatch($command);
7    return view('welcome');
8})->name('update');
9

L’évènement StatusCommandeMisAjour doit donc recevoir un objet Command:

 1<?php
 2
 3class StatusCommandeMisAjour implements ShouldBroadcast
 4{
 5    use Dispatchable, InteractsWithSockets, SerializesModels;
 6
 7    public $command;
 8
 9    public function __construct($command)
10    {
11        $this->command = $command;
12    }
13
14    public function broadcastOn()
15    {
16        return new Channel('commandes');
17    }
18}
19

Pour vérifier si tout fonctionne bien, lancer le serveur php (php artisan serve), naviger vers la route update et vérifier sur dashboard.pusher.com si l’évènement a été capturé.

Sur dashboard.pusher.com, aller dans l’onglet “Debug console” pour voir la liste des évènements capturés par pusher. Vous devriez voir le commande avec l’id 1:

Ecoute du message du coté Client

Maintenant qu’on a compri comment diffuser le message sur Pusher, nous allons voir comment écouter sur le channel donné avec Javascript, Laravel Echo, et pusher-js afin de réagir vis-à-vis du message.

Vu qu’on va modifier des fichiers javascript, il est nécessaire d’exécuter la commande ce compilation de npm.

1npm run watch

Dans resources/assets/js/bootstrap.js, décommenter les lignes correspondantes à Laravel-echo et comme dans config/broadcasting.php, mettre encrypted à false. A noter que le key et le cluster doivent respectivement correspondre au PUSHER_APP_KEY et au PUSHER_APP_CLUSTER dans le .env.

 1import Echo from 'laravel-echo'
 2
 3window.Pusher = require('pusher-js');
 4
 5window.Echo = new Echo({
 6    broadcaster: 'pusher',
 7    key: process.env.MIX_PUSHER_APP_KEY,
 8    cluster: process.env.MIX_PUSHER_APP_CLUSTER,
 9    encrypted: false
10});
11

Ensuite, on va modifier notre template welcome.blade.php et y ajouter le javascript compilé:

1<script src="/js/app.js"></script>

Maintenant, si vous naviguez sur la route update, vous constaterez dans le console de votre navigateur 2 erreurs:

  • La première est une erreur liée au CSRF-TOKEN. en effet, Laravel Echo a besoin d’avoir access au csrf-token donc on doit ajouter le meta correspondant dans le balise head du template:
1<meta name="crsf-token" content="{ { crsf_token() } }">
  • La seconde est une erreur lié au composant Vuejs. Par défaut, un projet Laravel utilise Vuejs, comme vous pouvez constater dans le fichier resources/assets/js/app.js:
 1require('./bootstrap');
 2
 3window.Vue = require('vue');
 4
 5Vue.component('example-component', require('./components/ExampleComponent.vue'));
 6
 7const app = new Vue({
 8    el: '#app'
 9});
10

On peut donc se défaire de cette erreur en ajoutant un élément avec un id app dans le template welcome.blade.php ou tout simplement enlever l’utilisation de Vuejs dans le projet, mais vu que dans la deuxième partie on va utiliser du Vuejs, on va juste ajouter un id app à un élément dans notre template:

1<body>
2    <div id="app" class="flex-center position-ref full-height">

Maintenant si on recharge la page, il ne dervait plus y avoir d’erreurs, néanmoins on constate que rien ne se passe à part l’envoie du message en arrière plan sur https://dashboard.pusher.com

Afin de mettre en évidence qu’on a bien entendu le message, nous allons afficher le message dans le console du navigateur.

Dans resources/assets/js/bootstrap.js, ajouter les codes suivants:

1window.Echo.channel('commandes')
2    .listen('StatusCommandeMisAjour', e => {
3        console.log('La commande avec l\'id '+e.command.id+' a été mise à jour');
4        console.log(e);
5    });
6
  • la méthode channel correspond au channel publique “Channel”. Noter que cette méthode varie selon le type de channel utilisé (ex: private pour les PrivateChannel)
  • “commandes” est le nom de notre channel dans app/Events/StatusCommandeMisAjour.php
  • “StatusCommandeMisAjour” est le nom de l’évènement qu’on va écouter, par convention Echo utilise le nom complet de la classe mais on n’a pas besoin de le spécifier parce que Echo va assumer que la classe donnée se trouve dans le namespace App\Event
  • e est l’évènement qu’on va recevoir. A la reception du message, on va juste afficher un message dans le console.

Pour voir le résultat, il nous faut 2 navigateus différents:

  • Ouvrez deux navigateurs differents (navigateur A et B) cote à cote
  • Dans le navigateur A:
    • naviguer sur la route update
    • vider le contenu du console.
  • Dans le navigateur B:
    • naviguer sur la route update
  • Dans le console du navigateur A, vous devriez apercevoir le message comme dans la figure suivante:

Maintenant qu’on a vu la base de Laravel Echo, on va approfondir sur les private channels avec un exemple de projet en utilisant du Vuejs coté client.

Exemple de projet

Pour nous aider à mieux comprendre les privates channels, nous allons par la suite mettre en place un simple plateforme de saisi d’éléments. Vous pouvez avoir accès au code source de cet exemple de projet sur Github

Pour ce faire, créer un projet Laravel et installer les dépendences nécessaires comme dans la section Configuration. Nous allons utiliser les mêmes configurations Pusher que ceux qu’on a vu précédemment.

Migration, models et routes

Configuer la connection à la base de données, créer une migration pour la table “elements” en y ajoutant un champs ‘details’ et executer la migration.

1public function up()
2{
3    Schema::create('elements', function (Blueprint $table) {
4        $table->increments('id');
5        $table->text('details');
6        $table->timestamps();
7    });
8}

Créer un model pour l’élément:

1php artisan make:model Element

Dans routes/web.php, ajouter les routes suivants:

 1// affichage de la liste de élément
 2Route::get('/home', function () {
 3    return view('welcome');
 4});
 5
 6// obtention de la liste des éléments
 7Route::get('/elements', function () {
 8    return Element::latest()->pluck('details');
 9});
10
11// création de nouvel élément
12Route::post('/elements', function () {
13    Element::forceCreate(request(['details']));
14});

Vuejs

Nous allons utiliser un composant Vuejs pour le listing des éléments.

Modifier le nom du fichier resources/assets/js/components/ExampleComponent.vue en ElementList.vue et editer son contenu:

 1<template>
 2    <div>
 3        <ul>
 4            <li v-for="element in elements" v-text="element"></li>
 5        </ul>
 6
 7        <button @click="addElement">Ajouter un élément</button>
 8        <input type="text" v-model="newElement">
 9    </div>
10</template>
11
12<script>
13    export default {
14        data() {
15            return {
16                elements: [],
17                newElement: ''
18            };
19        },
20
21        created() {
22            axios.get('/elements').then(response => (this.elements = response.data));
23        },
24
25        methods: {
26            addElement() {
27                axios.post('/elements', { details : this.newElement });
28
29                this.elements.push(this.newElement);
30
31                this.newElement = '';
32            }
33        }
34    }
35</script>

Noter que:

- dans *created()* on fait une requête GET pour avoir la liste des éléments
- dans *addElement()* on fait une requête POST pour créer une élément

Importer le composant ElementList dans resources/assets/js/app.js:

 1require('./bootstrap');
 2
 3window.Vue = require('vue');
 4
 5Vue.component('element-list', require('./components/ElementList.vue'));
 6
 7const app = new Vue({
 8    el: '#app'
 9});
10

Dans le template resources/views/welcome.blade.php, éditer le content en y ajoutant une référence au composant ElementList:

 1<body>
 2    <div id="app" class="flex-center position-ref full-height">
 3        @if (Route::has('login'))
 4            <div class="top-right links">
 5                @auth
 6                    <a href="{ { url('/home') } }">Home</a>
 7                @else
 8                    <a href="{ { route('login') } }">Login</a>
 9                    <a href="{ { route('register') } }">Register</a>
10                @endauth
11            </div>
12        @endif
13
14        <div class="content">
15            <element-list></element-list>
16        </div>
17    </div>
18
19    <script src="/js/app.js"></script>
20</body>

Evènement

Ensuite, créer un évènement ElementCreated :

1php artisan make:event ElementCreated
 1class ElementCreated implements ShouldBroadcast
 2{
 3    use Dispatchable, InteractsWithSockets, SerializesModels;
 4
 5    public $element;
 6
 7    public function __construct($element)
 8    {
 9        $this->element = $element;
10    }
11
12    public function broadcastOn()
13    {
14        return new Channel('elements');
15    }
16}

Noter qu’on utilise encore le channel publique “Channel”, mais on va bientôt basculer vers le PrivateChannel.

Diffusion sur Pusher

Maintenant qu’on a notre évènement, on peut le diffuser sur pusher. Allez dans le fichier routes/web.php et diffiser l’évènement après la création de l’élément:

1// création de nouvel élément
2Route::post('/elements', function () {
3    $element = Element::forceCreate(request(['details']));
4
5    event(new ElementCreated($element));
6});

Pour vérifier si tout fonctionne bien, naviguer sur la route “/home”, entrer un “Premier élément” et cliquer sur “Ajouter un élément”:

Grace à Vuejs, vous devriez tout de suite voir apparaître en dessous du bouton l’élément que vous venez d’ajouter.

Et du coté de dashboard.pusher.com, vous constaterez que l’évènement a bien été diffisé et capturé.

Ecoute coté client

Nous allons maintenant écouter sur le channel “elements” et mettre à jour la liste de nos éléments à chaque fois qu’un nouvel élément ait été créé.

Dans le handler created du composant ElementList resources/assets/js/components/ElementList.vue, ajouter l’instruction qui correspond à l’écoute sur le channel “elements” de l’évènement “ElementCreated”:

1created() {
2    axios.get('/elements').then(response => (this.elements = response.data));
3
4    window.Echo.channel('elements').listen('ElementCreated', e => {
5        this.elements.push(e.element.details);
6    })
7},
8

Pour constater le résultat, ouvrez deux ou plusieurs navigateurs différents cote à cote et allez sur la route “/home”.

Depuis l’un des navigateurs, ajouter un “Deuxième élément”. Vous constaterez que la liste des éléments de tous les autres navigateurs ont été mise à jour en temps réel.

Mais vous constaterez également que le “Deuxième élément” se presente 2 fois dans la liste du navigateur avec lequel vous l’avez ajouté.

En effet, cela est dû au faite que dans resources/assets/js/components/ElementList.vue:

  • lorsqu’on clique sur le bouton Ajouter un élément, la méthode addElement() envoie une requête POST pour créer un nouvel élément, ensuite ajoute l’élément ainsi créé dans this.elements
1addElement() {
2    axios.post('/elements', { details : this.newElement });
3
4    this.elements.push(this.newElement);
5
6    this.newElement = '';
7}
8
  • et comme on a branché la diffusion de l’évènement ElementCreated après avoir créer un élément, lorsqu’on reçoit un message qu’un élément a été créé, on ajout aussi l’élément recu dans this.elements
1created() {
2    axios.get('/elements').then(response => (this.elements = response.data));
3
4    window.Echo.channel('elements').listen('ElementCreated', e => {
5        this.elements.push(e.element.details);
6    })
7}
8

Du coup, la solution serait de diffuser l’évènement ElementCreated à tout le monde sauf à celui qui a ajouté l’élément.

Pour ce faire, on doit modifier l’évènement ElementCreated et utiliser la méthode dontBroadcastToCurrentUser() défini dans le trait Illuminate\Broadcasting\InteractsWithSockets

1public function __construct($element)
2{
3    $this->element = $element;
4
5    $this->dontBroadcastToCurrentUser();
6}

Essayer d’ajouter un “Troixième élément” pour voir le resultat:

Private Channel

Pour utilisr le PrivateChannel, nous avons besoin de faire quelques modifications.

  • Il faut ajouter l’authentification à notre projet afin de pouvoir créer des utilisateurs:
1php artisan make:auth
  • Créer 3 utilisateurs.
  • Ajouter un nouveau champ “role” dans la table “users” et donner le role “admin” pour les 2 premiers utilisateurs et “simple” pour le troixième.

Avant de passer en PrivateChannel, il nous faut définir une logique d’authentification.

Supposons que les utilisateurs sont classés par rôle (admin / simple) et définissons la logique suivante:

Tous les utilisateurs dont le rôle est égal à “admin” voient leur liste d’éléments mises à jour en temps réel dès que l’un des autres utilisateurs ajoute un nouvel élément.

Il va donc falloir implémentent un PrivateChannel pour seulement les utilisateurs “admin”.

Allez dans l’évènement App\Events\ElementCreated et modifiez celui-ci en PrivateChannel.

1public function broadcastOn()
2{
3    return new PrivateChannel('elements');
4}

Allez dans resources/assets/js/components/ElementList.vue et modifier la méthode channel en private:

1created() {
2    axios.get('/elements').then(response => (this.elements = response.data));
3
4    window.Echo.private('elements').listen('ElementCreated', e => {
5        this.elements.push(e.element.details);
6    })
7}
8

Dans routes/channels.php ajouter:

1Broadcast::channel('elements', function ($user) {
2    if ($user->role === "admin") {
3        return true;
4    }
5
6    return false;
7});

Pour faire simple nous comparons juste le role de l’utilisateur en cours à la valeur “admin”, si les deux sont égaux, on accepte l’authentification sinon on refuse.

Vouz constaterez que si vous naviguez vers “/home”, il y a un erreur 403 dans le console du navigateur.

1POST http://127.0.0.1:8000/broadcasting/auth 403 (Forbidden)

Et de même si vous vous connectez avec le troixième utilisateur dont le role est égal à “simple”, vous auriez également l’erreur 403.

Maintenant, ouvrez les comptes des 3 utilisateurs (2 admin et 1 simple) dans 3 navigateurs différents, ajouter des éléments depuis l’un des comptes utilisateurs, vous deviez constater qu’effectivement la liste des éléments des deux utilisateurs “admin” se met à jour automatiquement à chaque saisi de nouvel élément depuis n’importe quel utilisateur.

Laravel Echo est assez simple à utiliser. Avec simplement quelques lignes de codes, on peut implémenter des fonctionnalités assez complexes. En suivant l’éxemple de plateforme de saisi d’éléments, vous pouriez facilement arriver à mettre en place un système de chat entre des utilisateurs.