w3resource

Laravel (5.7) Broadcasting

In many modern web applications, WebSockets are used to implement realtime, live-updating user interfaces. When some data is updated on the server, a message is typically sent over a WebSocket connection to be handled by the client. This provides a more robust, efficient alternative to continually polling your application for changes.

To assist you in building these types of applications, Laravel makes it easy to "broadcast" your events over a WebSocket connection. Broadcasting your Laravel events allows you to share the same event names between your server-side code and your client-side JavaScript application.

Review Config:

You'll need to ensure your config is setup to inject your Pusher credentials.

'pusher' => [
   'driver' => 'pusher',
   'key' => env('PUSHER_APP_KEY'),
   'secret' => env('PUSHER_APP_SECRET'),
   'app_id' => env('PUSHER_APP_ID'),
   'options' => [
       'cluster' => env('PUSHER_APP_CLUSTER'),
       'encrypted' => true,
   ],
],

App\Providers\BroadcastServiceProvider.php

Via the Docs:

Before broadcasting any events, you will first need to register the App\Providers\BroadcastServiceProvider. In fresh Laravel applications, you only need to uncomment this provider in the provider's array of your config/app.php configuration file. This provider will allow you to register the broadcast authorization routes and callbacks.

routes/broadcasting.php

Broadcasting routes are basically simple guards that will check an entity before allowing a private connection. Users must be authorized before connecting to private channels. We only need the default authentication channel to access our notifications for this demo.

Channel routes are authorization callbacks used to check if an authenticated user can listen to a channel. Instead of returning data, they return the authorized status for the private channel. Essentially they are routes that respond with only the guard status.

Endpoint: 'http://localhost/broadcasting/auth'

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

**The name of each route reflects the namespace of the entity that requires authorization.

Broadcasting Service Provider

In order to use our API routes with broadcasting routes. We must first update our BroadcastServiceProvider to use "auth:api" instead of "web".

class BroadcastServiceProvider extends ServiceProvider
{
   /**
    * Bootstrap any application services.
    * @return void
    */
   public function boot()
   {
       Broadcast::routes(["middleware" => ["auth:api"]]);
       require base_path('routes/channels.php');
   }
}

Login Method

You can load the current User's notifications when they login. This will allow you to seed existing notifications into your component without an extra HTTP request. (You'll need to assign the property to your reactive data)

response.data.currentUser.notifications

return response(array(
  'currentUser' => $user->loadMissing('notifications'),
));

Component Setup

Let's setup our component scaffolding. We'll need to import our JS libraries as well as setup the properties and template.

import Echo from 'laravel-echo'
import Pusher from 'pusher-js'
export default {
  name: 'laravel-echo',
  data() {
     return {
        echo: null
     }
  },
  computed: {
     //Add computed properties
    currentUser: { ...},
    isConnected: { ...},
    notifications: { ...},
  },
  watch: {
     //Add watchers...
  },
  methods: {
     //Add methods...
  }
}
<template>
  <div id="laravel-echo">
     <template v-if="isConnected">
        <ul v-for="object in notifications">
           {{ object }}
        </ul>
        <button @click="disconnect">Disconnect</button>
     </template>
     <template v-else-if="currentUser">
        <button @click="connect">Connect</button>
     </template>
  </div>
</template>

Computed Properties

Next we'll need our authenticated user as well as the state of our connection and reactive storage property. In this example I'm using Vuex State Management. (you can use local properties instead of computed if you’re not using Vuex)

currentUser: {
 cache: false,
 get(){ return this.$store.getters.currentUser }
},
notifications: {
 cache: false,
 get(){ return this.$store.getters.notifications.reverse() }
},
isConnected: {
cache: false,
get(){
return (this.echo &&   this.echo.connector.pusher.connection.connection !== null)
}
},

(currentUser, isConnected)

We can watch our state and automatically connect when the component is loaded and the currentUser is present as well as automatically disconnect when the user is null.

watch: {
 currentUser: {
   handler(currentUser){
    (currentUser !== null ? this.connect() : this.disconnect())
   }
 },
 isConnected: {
   handler(isConnected){       
     this.$emit('broadcasting:status', isConnected)
   }
 }
},

Add Method: connect()

In this example we'll be using the native Laravel api_token auth instead of web & CSRF./** Connect Echo **/

connect(){
  if(!this.echo){
     this.echo = new Echo({
        broadcaster: 'pusher',
        key: 'XXX',
        cluster: 'us2',
        encrypted: true,
        authEndpoint: 'http://localhost/broadcasting/auth',
        auth: {
           headers: {
              Authorization: null
           }
        },
        //csrfToken: null,
        //namespace: 'App',
     })
     this.echo.connector.pusher.connection.bind('connected', (event) => this.connect(event))
     this.echo.connector.pusher.connection.bind('disconnected', () => this.disconnect())
  }
  this.echo.connector.pusher.config.auth.headers.Authorization = 'Bearer ' + this.currentUser.api_token
  this.echo.connector.pusher.connect()
}

Add Method: bindChannels()

The bindChannels() method will bind the channel callbacks that commit each message to our $store (or will assign to local properties). *Note that Echo provides a method specifically for handling notifications. ** Because we're dealing with an Array type, you'll want to push() a new item into the array.

/** Bind Channels **/

bindChannels(){
  let vm = this
  this.echo.private('App.User' + '.' + this.currentUser.id)
.notification((object) => vm.$store.commit('addNotification', object))
},
or for local data use:
this.echo.private('App.User' + '.' + this.currentUser.id)
.notification((object) => vm.notifications.push(object))

Add Method: disconnect()

The disconnect() method will allow us to toggle our connection on/off.

/** Disconnect Echo **/

disconnect(){
  if(!this.echo) return
  this.echo.disconnect()
},

Component Usage:

If you're using Vue Router like I am you'll want to place this in your root component so it will continue to work as users navigate the app.

<router-view/>...
<broadcasting ref="broadcasting"/>
this.$refs.broadcasting.connect()
this.$refs.broadcasting.disconnect()

Part 3: Notifications

php artisan make:notification TestBroadcastNotification

In this example we're using notifications.

<?php namespace App\Notifications;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\BroadcastMessage;
class TestBroadcastNotification extends Notification
{
   use Queueable;
   public $object;
/**
    * Create a new notification instance.
    * @param $object object
    * @return void
    */
   public function __construct($object)
   {
       $this->object = $object;
   }

   /**
    * Get the notification's delivery channels.
    * @param  mixed  $notifiable
    * @return array
    */
   public function via($notifiable)
   {
       return [
          //'database', (migration required)
          //'mail',
          'broadcast',
       ];
   }

  /**
   * Get the broadcastable representation of the notification.
   * @param  mixed  $notifiable
   * @return BroadcastMessage
   */
  public function toBroadcast($notifiable)
  {
     $timestamp = Carbon::now()->addSecond()->toDateTimeString();
     return new BroadcastMessage(array(
        'notifiable_id' => $notifiable->id,
        'notifiable_type' => get_class($notifiable),
        'data' => $this->object,
        'read_at' => null,
        'created_at' => $timestamp,
        'updated_at' => $timestamp,
     ));
  }
   /**
    * Get the mail representation of the notification.
    *
    * @param  mixed  $notifiable
    * @return \Illuminate\Notifications\Messages\MailMessage
    */
   public function toMail($notifiable)
   {
       return (new MailMessage)
                   ->line('The introduction to the notification.')
                   ->action('Notification Action', url('/'))
                   ->line('Thank you for using our application!');
   }

   /**
    * Get the array representation of the notification.
    * @param  mixed  $notifiable
    * @return array
    */
   public function toArray($notifiable)
   {
       return (array) $this->object;
   }
}

toBroadcast() Method

Because our notification has not been saved to the database yet, we'll need to mock our notification object to match what's loaded from our database when we query the user's notifications normally. We can use Carbon to create a timestamp 1 second in the future to match our actual notification when it's finally saved. (if you have a better way of dealing with this, please share!)

$timestamp = Carbon::now()->addSecond()->toDateTimeString();
     return new BroadcastMessage(array(
        'notifiable_id' => $notifiable->id,
        'notifiable_type' => get_class($notifiable),
        'data' => $this->object,
        'read_at' => null,
        'created_at' => $timestamp,
        'updated_at' => $timestamp,
     ));

Add: Test Route

routes/api.php

Route::any('test', function(){
  $user =  \App\User::findOrFail(1);
  $data =  (object) array(
     'test' => 123,
  );
  $user->notify(new \App\Notifications\TestBroadcastNotification($data));
  return response()->json($data);
});

Previous: Laravel (5.7) Artisan Console
Next: Laravel (5.7) Cache