Notification Brodacast System with Laravel-Websocket and Rxjs

enrico_dev86

Enrico Bellanti

Posted on June 20, 2022

Notification Brodacast System with Laravel-Websocket and Rxjs

Why Laravel and Rxjs?

This is my first post and i would like to explain how i resolve my issue.
Obviously you can find many tutorial to implement a websocket but sometimes you can get into my same situation when you are working with Laravel at back-end and Angular at front-end more specifically with Rxjs.
I tried to find some tutorial online which suggest to use laravel-websockets and laravel-echo which is a very common pattern but if you are using Rxjs is not the best solution.

Installing Laravel Websockets
Require the Laravel Websockets package. It works as a replacement for external services like Pusher. Many settings will refer to Pusher today but be reminded that we are not using it. We want our own solution.

composer require beyondcode/laravel-websockets
Enter fullscreen mode Exit fullscreen mode

We also need a package by Pusher.

composer require pusher/pusher-php-server
Enter fullscreen mode Exit fullscreen mode

Next, adapt your .env file. We want the BROADCAST_DRIVER to be pusher.

BROADCAST_DRIVER=pusher
Enter fullscreen mode Exit fullscreen mode

And we need to set the Pusher credentials.
(Note: Again I want to mention that we do not use the Pusher service. Our websockets server just has the same API.)

PUSHER_APP_ID=12345
PUSHER_APP_KEY=12345
PUSHER_APP_SECRET=12345
PUSHER_APP_CLUSTER=mt1
Enter fullscreen mode Exit fullscreen mode

The Laravel Websockets package comes with a migration file for storing statistics and a config file we need to adapt. Let's publish them.

php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations"
Enter fullscreen mode Exit fullscreen mode

This will create a new migration file that we can run. Make sure you have set up a database for this project and defined the DB credentials in the .env file. Afterward, we can run the migration.

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

And here, we publish the config file of Laravel Websockets.

php artisan websockets:serve
Enter fullscreen mode Exit fullscreen mode

To test that it is running, we can check the debugging dashboard under the endpoint /laravel-websockets. You can click connect to see if the dashboard can connect to the WebSockets server.
laravel-websockets-screen

After clicking connect, you should see that the dashboard is subscribed to some debugging channels like private-websockets-dashboard-api-message. This will tell you that the server is set up correctly.

Broadcast Notifications From Our Laravel Application
We can use notifications to send data to our WebSockets server. So let's create a new one.

php artisan make:notification RealTimeNotification
Enter fullscreen mode Exit fullscreen mode

Here is what we need to change:

  • use the ShouldBroadcast interface
  • add a message property which we will pass through the constructor
  • use the broadcast channel in the via method
  • add a toBroadcast method to define the message
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\BroadcastMessage;

class RealTimeNotification extends Notification implements ShouldBroadcast
{
    use ShouldQueue;

    public string $message;

    public function __construct(string $message)
    {
        $this->message = $message;
    }

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

    public function toBroadcast($notifiable): BroadcastMessage
    {
        return new BroadcastMessage([
            'message' => "$this->message (User $notifiable->id)",
            'event' => class_basename($this),
        ]);
    }

}
Enter fullscreen mode Exit fullscreen mode

Before we can try sending this event, please adapt your broadcasting.php config file to use the following options:

'options' => [
    'cluster' => env('PUSHER_APP_CLUSTER'),
    'encrypted' => false,
    'host' => '127.0.0.1',
    'port' => 6001,
    'scheme' => 'http'
],
Enter fullscreen mode Exit fullscreen mode

With these options, we make sure that when we broadcast something from our Laravel application, it gets sent to our WebSockets server.

Let's have a test if everything is working properly.
Let's trigger the nofification and check in the websockets dashbord if you have sucess.

$user = User::first();

$user->notify(new App\Notifications\RealTimeNotification('Hello World'));
Enter fullscreen mode Exit fullscreen mode

websockets-dashboard-notification-test

Time to connect back-end to fornt-end

Let's have a look at code to see how to connect front-end with Rxjs with laravel-websockets

import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { filter, map, Subject, takeUntil } from 'rxjs';
import { IAuthTokenWs } from './shared/interfaces/webSocket.interface';
import { WebsocketService } from './shared/services/webSocket/websocket.service';
import { selectUserId } from './store/user/user-feature.selectors';

@Component({
  selector: 'hh-root',
  template: `
    <bn-loading-spinner></bn-loading-spinner>
    <router-outlet></router-outlet>
  `,
  styleUrls: ['./app.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnDestroy {
  destroy$$ = new Subject<void>();

  constructor(private websocketService: WebsocketService, private store: Store) {
    this.store
      .select(selectUserId)
      .pipe(
        takeUntil(this.destroy$$),
        filter((e) => !!e),
        map((user_id) => {
          let socket$ = new WebSocket('ws://localhost:6001/app/12345');

          socket$.onmessage = (msg) => {
            let obj = JSON.parse(msg.data);

            if (obj.event === 'pusher:connection_established') {
              let socket_id = JSON.parse(obj.data).socket_id;
              this.websocketService
                .authWebsocket(user_id!!, socket_id)
                .pipe(takeUntil(this.destroy$$))
                .subscribe((e: IAuthTokenWs) => {
                  let tmp = JSON.stringify({
                    event: 'pusher:subscribe',
                    data: {
                      auth: e.auth,
                      channel: `private-App.Models.User.${user_id}`,
                    },
                  });
                  socket$.send(tmp);
                });
            } else {
              this.websocketService.notifications.next([
                ...this.websocketService.notifications.value,
                JSON.parse(obj?.data)?.message,
              ]);
              console.log(JSON.parse(obj?.data)?.message);
            }
          };
        }),
      )
      .subscribe();
  }

  ngOnDestroy(): void {
    this.destroy$$.next();
  }
}

Enter fullscreen mode Exit fullscreen mode

You probably noticed that we did not define a channel name with our notification as we did in our event. This is because there is a default pattern for the channel name of a notification notifiable-class.key. In our case, this would be App.Models.User.1. And when you take a look at the Web dashboard, you find a message triggered by our notification to the channel Channel: private-App.Models.User.1.

Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
    return (int) $user->id === (int) $id;
});
Enter fullscreen mode Exit fullscreen mode

In our case we are using Ngrx aswell so we will get user_id with
.select(selectUserId), but obiusly you can get anywhere you have stored it.
Also in this application we use an interceptor which attach token to api call to be authorize by laravel guard at backend when we need to call the following api to retrive token auth to listen on private channel via websocket.

  authWebsocket(userID: string, socket_id: string) {
    const data = {
      socket_id,
      channel_name: `private-App.Models.User.${userID}`,
    };

    return this.http.post<IAuthTokenWs>(`${this.apiBaseUrl}/broadcasting/auth`, data);
  }
Enter fullscreen mode Exit fullscreen mode

After implemented it when triggering notification at back-end you should recive a console.log in your application front-end.
Now your BE and FE are connected.

Considering you can receive notification in broadcast just if you are connected to the web application in real time broadcasting, probably when you are offline you won't get them back after you reconnect, there are several methods to implement this and i would like to make soon a new short tutorial "how to store notification unseen and retrives in specific moment" (for example immediatelly after login).

Note: consider to implement Laravel Queue System with notifications to avoid that your faild ones never delivered to you receivers. I just wrote a an additional guide for it.notification-queue-tutorial

💖 💪 🙅 🚩
enrico_dev86
Enrico Bellanti

Posted on June 20, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related