Notifier ses utilisateurs via les websockets

Publié le 23 mai 2022 par William Suppo
Couverture de l'article Notifier ses utilisateurs via les websockets

Dans ce tutoriel, on va s'appuyer sur un outil puissant de Laravel pour notifier facilement nos utilisateurs via websocket.

Le contexte

Sur notre site d’e-commerce nous allons mettre en place une notification lors de la livraison d’une commande à un utilisateur.

Avant de s’attaquer au cœur du sujet de notre tutoriel, on va créer les briques nécessaires à son élaboration.

On va tout d’abord créer notre modèle Order qui est lié au modèle User avec une relation belongsTo ce qui signifie qu’une commande est associé à un seul utilisateur, voici sa classe :

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Database\Eloquent\Model;
7 
8class Order extends Model
9{
10 use HasFactory;
11 
12 protected $fillable = [
13 'name', 'amount',
14 ];
15 
16 public function user()
17 {
18 return $this->belongsTo(User::class);
19 }
20}

Et la migration qui définit sa structure en base :

1<?php
2 
3use Illuminate\Database\Migrations\Migration;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Support\Facades\Schema;
6 
7return new class extends Migration
8{
9 public function up()
10 {
11 Schema::create('orders', function (Blueprint $table) {
12 $table->id();
13 $table->timestamps();
14 $table->string('name');
15 $table->double('amount');
16 $table->foreignIdFor(\App\Models\User::class);
17 });
18 }
19 
20 public function down()
21 {
22 Schema::dropIfExists('orders');
23 }
24};

On établit aussi la relation inverse dans le modèle User :

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Contracts\Auth\MustVerifyEmail;
6use Illuminate\Database\Eloquent\Factories\HasFactory;
7use Illuminate\Foundation\Auth\User as Authenticatable;
8use Illuminate\Notifications\Notifiable;
9 
10class User extends Authenticatable
11{
12 // ...
13 
14 public function orders()
15 {
16 return $this->hasMany(Order::class);
17 }
18}

Ensuite, nous allons créer John, notre aimable client qui est ravi de nous aider dans notre périple, en utilisant la commande php artisan tinker :

Untitled

Enfin, on crée une route qui nous permet de générer une commande et d'afficher son nom en sortie, celle-ci est assignée à notre super client John :

1Route::get('/orders/save', function () {
2 $user = \App\Models\User::where('name', 'John')->first();
3 
4 $order = \App\Models\Order::make([
5 'name' => \Illuminate\Support\Str::random(8),
6 'amount' => 19.89,
7 ]);
8 
9 $user->orders()->save($order);
10 
11 echo $order->name;
12});

Création de la notification

Le système de notification de Laravel est très complet, on va s’appuyer sur ce dernier pour faciliter grandement la mise en place de notre fonctionnalité.

On commence par créer notre nouvelle classe App\Notifications\OrderDelivered :

1<?php
2 
3namespace App\Notifications;
4 
5use Illuminate\Notifications\Notification;
6 
7class OrderDelivered extends Notification
8{
9}

On y ajoute un constructeur qui a pour seul paramètre la commande dont la livraison a lieu :

1public function __construct(public Order $order) {}

Une notification peut-être envoyée via un ou plusieurs canaux déterminés au sein de la méthode via. Pour nos besoins, on choisit le canal broadcast nécessaire pour le websocket :

1public function via($notifiable): array
2{
3 return ['broadcast'];
4}

A noter, le paramètre $notifiable qui est l’instance de l’utilisateur cible de la notification, ce dernier peut servir pour une sélection particulière de canaux, dans l'éventualité où on a implémenter la logique.

Pour chacun des canaux, il est nécessaire d’implémenter la méthode correspondante qui sert à la préparation des données spécifiques de celui-ci. Dans notre cas, on implémente toBroadcast qui doit renvoyer un objet de type Illuminate\Notifications\Messages\BroadcastMessage :

1public function toBroadcast($notifiable)
2{
3 return new BroadcastMessage([
4 'order' => $this->order,
5 ]);
6}

A noter, encore une fois, que notre utilisateur est disponible via $notifiable et par conséquent, on pourrait envisager un traitement particulier des données en fonction de ce dernier.

Et voilà, notre notification est prête, reste à l’utiliser.

Déclencher l’envoi

Maintenant, on va s’attarder à provoquer l’envoi de la notification. Pour cela, on déclare une route qui simulera la livraison de notre commande et qu’il est temps de notifier son propriétaire :

1Route::get('/orders/deliver/{order:name}', function (\App\Models\Order $order) {
2 $user = $order->user;
3 
4 $user->notify(new \App\Notifications\OrderDelivered($order));
5 
6 echo 'Notification émise à ' . $user->name;
7});

Comme on peut le voir, on dispose d'une méthode User::notify() disponible part le trait Illuminate\Notifications\Notifiable qui accepte en paramètre une notification. On aurait tout à fait pu utiliser la façade Notification à la place.

Ensuite, dans le cadre de ce tutoriel on utilise le service Pusher pour la propagation des notifications via websocket, mais il est tout à fait possible d’utiliser son propre serveur.

Il nous ai donc nécessaire d’intégrer le paquet composer dédié :

1composer require pusher/pusher-php-server

De créer un compte sur le site de Pusher et de renseigner les variables d'environnement suivantes :

1PUSHER_APP_ID=your-pusher-app-id
2PUSHER_APP_KEY=your-pusher-key
3PUSHER_APP_SECRET=your-pusher-secret
4PUSHER_APP_CLUSTER=mt1
5 
6BROADCAST_DRIVER=pusher

Réception côté client

On commence par la configuration de notre client Javascript pour qu’il puisse écouter via websocket.

On installe donc la librairie Pusher et Laravel Echo :

1npm install --save-dev laravel-echo pusher-js

Ensuite on décommente les lignes suivantes dans le fichier resources\js\bootstrap.js :

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 forceTLS: true
10});

Maintenant que la configuration est terminée, on va pouvoir créer un composant VueJs (v3) qui va s’abonner au canal privé de l’utilisateur et afficher le contenu de la notification dans une liste :

1<template>
2 <div>
3 <h1>Notifications</h1>
4 <ul>
5 <li v-for="notification in notifications">
6 <div>La commande {{ notification.order.name }} d'un montant de {{ notification.order.amount }} a été livrée.</div>
7 </li>
8 </ul>
9 </div>
10</template>
11 
12<script>
13export default {
14 name: "Notifications",
15 
16 props: [
17 'user',
18 ],
19 
20 data() {
21 return {
22 notifications: [],
23 }
24 },
25 
26 created() {
27 Echo.private('App.Models.User.' + this.user)
28 .notification((notification) => {
29 this.notifications.unshift(notification);
30 });
31 }
32}
33</script>

La logique intéressante réside dans la méthode created(), où on demande à notre client JS d'écouter sur le canal privé de l'utilisateur fourni dans les props.

Ensuite on ajoute notre composant à l’initialisation de VueJs dans le fichier resources/js/app.js :

1require('./bootstrap');
2 
3import { createApp } from "vue";
4import Notifications from "./components/Notifications";
5 
6const app = createApp({
7 components: {
8 Notifications,
9 }
10});
11 
12app.mount("#app");

A cet instant, il est nécessaire de recompiler les librairies Javascript

Il nous reste à intégrer notre composant dans une vue, qu’on appel tuto, et de lui passer l’id de l’utilisateur connecté :

1<div id="app">
2 <notifications user="{{ Auth::user()->id }}"></notifications>
3</div>
4<script src="{{ asset('js/app.js') }}" defer></script>

Puis à définir la route pour y accéder :

1Route::get('/tuto', function () {
2 Auth::login(User::where('name', 'John')->first());
3 
4 return view('tuto');
5});

Petit point sécurité : comme on veut transmettre la notification a un utilisateur particulier, il est nécessaire qu’il soit authentifié car Laravel va passer par un canal privé qui lui est dédié et va donc vérifier son identité lors de l’appel côté client. Le Auth::login() ici présent n'est là que pour les besoins du tuto, à ne pas reproduire en l'état !

Et voilà, on peut dorénavant créer des commandes sur /orders/save, récupérer son nom et provoquer la notification sur /orders/deliver/xxxxxx pour enfin consulter les notifications sur le /tuto !

Pour aller plus loin

Afin de bonifier notre fonctionnalité on peut stocker les notifications en base pour permettre à nos utilisateurs de les consulter ultérieurement et pouvoir les marquer en lu.

Aussi, on peut créer un autre composant qui affichera une petite cloche avec une pastille lorsque des nouvelles notifications seront disponibles pour nos utilisateurs !

William Suppo avatar
William Suppo
Je mange du PHP au p'tit dej', et vous ?

A lire

Autres articles de la même catégorie