laravel echo server redis socket io

Demonstrating Laravel echo, socket io and redis with real world example

In this post i will demonstrate how to use sockets in laravel framework using the new laravel package laravel echo server, redis and socket io.

 

 

Requirements

  • Laravel framework 5.6 or higher
  • Knowledge of laravel events
  • Knowledge of laravel-echo-server
  • socket io

 

Sockets and Broadcasting

Laravel framework always simplify development of web applications using it’s unique features, today i will show one important feature of these which is how to use sockets in laravel with the help of laravel broadcasting. If you refer to laravel docs here you will see a detailed explanation of how to setup broadcasting and how to use it, but let’s first understand what broadcasting means.

Broadcasting always comes with in combination events which means emit an event from one side and catch that event from the other side, the first side here refers to a server and the second side is a client so the server emit events and the client catches and listens for events and do some logic with them. This server works via a socket connection, once this socket connection is turned on it listens for any event that happens in the enclosing scope.

For example if we take a famous example which broadcasting applied mostly is a chat application, in such application specific user joins then another user joins, the socket server notices that then it notifies both users of presence of each other by emitting a presence event. On the other hand if one user sends a message to the other then the server emits an event for sending message, then the other user listens for that message and displays it.

 

Concepts

Channel: A channel in laravel broadcasting is like a tv channel where you register events in that channel and then listen for it, there are different types of channels (Public, Private and Presence). A single channel can contain multiple events.

Event: The event is the core object of using sockets where it defines the item to be emitted to the clients. Events must belong to a channel, in laravel you define the event using a class that implements ShouldBroadcast or ShouldBroadcastNow interface and to fire it you use whether Event::fire(‘event_name’) or event(new EventName()).

 

Laravel Supports multiple broadcasting drivers which are pusher, redis and log. Pusher is simple to use and setup but unfortunately it is not free so In this tutorial i will instead use redis and socketIO.

 

Example overview

To illustraite the idea i will create a simple laravel application, in this example we have a real-estate website where we have two types of users. The first type called “host” and is capable of adding listings and the other type is the “guest” where he is able to view the listing and book it. Now imagine that a new requirement arises, we want to display a notification to the user when another user displays the same listing page so we want to display this notification another user looking at this listing right now .

 

Project setup

Let’s setup a new laravel 5.6 project with composer:

composer create-project laravel/laravel broadcasting 5.6 --prefer-dist

change your db settings:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=<db name>
DB_USERNAME=<db user>
DB_PASSWORD=<db password>

Next lets create a migration for listings table:

php artisan make:migration create_listing_table

modify database/migrations/XXXX_XX_XX_XXXXX_create_listing_table.php

....
....
....
class CreateListingTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('listing', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->text('description');
            $table->string('image');
            $table->integer('price');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('listing');
    }
}

Now migrate

php artisan migrate

 

Next let’s insert some dummy data into our table so we will use laravel seeder

Open database/seeds/DatabaseSeeder.php and modify it like this:

<?php

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // $this->call(UsersTableSeeder::class);

        $data = [
          [
              "title" => "Staten Island Urby 8 Navy Pier Ct, Staten Island, NY 10304 ",
              "description" => "
At the heart of Staten Island’s new North Shore waterfront, Urby is a total rethink for a new generation of apartment seekers. Our brand new studio, one- and two-bedroom apartments are designed to make life easy. Each rental features built-in storage and custom closets, in-home washer/dryer and oversized windows for amazing light and views of NYC. Built for convenience, there is a cafe in the lobby to keep residents powered up, communal kitchen with chef-in-residence, and digital package pick-up. Friendly to any type of commute, Urby is located steps from the Stapleton Train Stop and has parking available to residents along with a bike room. Urby also has amenities that support a healthy lifestyle including an outdoor pool, 2-floor indoor Technogym(R) fitness center, filtered communal well, and easy access to a brand new waterfront esplanade.
",
              "image" => "https://ar.rdcpix.com/656902932/7c0306244b27c2c56809563467b4fcdec-f0xd-w1020_h770_q80.jpg",
              "price" => "1850"
          ],
          [
              "title" => "The Drake 6260 99th St, Rego Park, NY 11374 ",
              "description" => "
An incomparable blend of style and sophistication awaits you at The Drake, featuring 420 new, luxury no-fee Rego Park apartments for rent. The Drake combines exceptional taste, modern design, and classic, understated elegance. The Drake's no-fee Rego Park rentals raise the bar with distinctive interiors, deluxe amenities, and a nod to its Queens heritage. One-to four-bedroom residences are available, all with flexible layouts and outdoor space. This Queens luxury rental building reflects the spirit of the original Drake, which was once a lively neighborhood movie theater. The Drake elevates your comfort and convenience in every aspect of life, as residents will enjoy access to an impressive suite of amenities. The Drake has a doorman and a resident lounge, a state-of-the-art fitness and wellness center, bike storage, laundry facilities, a spa and grooming center for pets, and a colorful playroom. The Drake welcomes you to a thriving culturally rich neighborhood, Rego Park, in the heart of Queens. With restaurants, entertainment, and transit right at your fingertips. And for all the possibilities for work and play, the M and R trains mean Manhattan is always just a quick ride away from your no-fee Queens apartment at The Drake.
",
              "image" => "https://ar.rdcpix.com/105515697/1a9acbed962760087f333e2ca9335b77c-f0xd-w1020_h770_q80.jpg",
              "price" => "2400"
          ],
            [
                "title" => "EOS 100 W 31st St, New York, NY 10001 ",
                "description" => "
EOS offers distinctive residences, curated amenities and personalized service needed to engage New York City life to the fullest — inside and out. Premier recreation and relaxation facilities for residents to enjoy include a pool, fitness center on the lower level as well as a game room and entertaining areas on the tower’s 47th floor with sweeping views of Manhattan. EOS offers 375 smoke-free rental residences consisting of studios, one, two and two bedroom + den layouts. Each apartment contains a washer/dryer, dishwasher, hardwood floors, kitchens and bathrooms with premium finishes and fixtures. Consistent with The Durst Organization’s ongoing commitment to sustainability, EOS offers residents a vision of sustainability that both respects the natural world and promotes your wellbeing. Centered around the four core elements— Water, Air, Earth and Energy— EOS gives back to the environment and provides a home where people can thrive. Sustainable features have been woven throughout the building and each residence as an integral part of its design, construction and management.Stepping out from inside EOS, some of the most desirable neighborhoods await. Electric NoMad, with its mix of restaurants, hotels and boutiques, is at your doorstep. Stroll to Midtown, Bryant Park, the Flatiron District, Chelsea and the Meatpacking District. EOS residents are also within a five block walk to the following transit lines: 1, 2, 3, 6, B, D, F, M, N, Q, R, A, C, E, PATH, Amtrak trains and various bus routes. EOS provides residents the best that New York City has to offer, both inside and out. Come discover a neighborhood that immediately feels like home.
",
                "image" => "https://ar.rdcpix.com/1331050435/83614eb36fe85bbd00c328f3b2628899c-f0xd-w1020_h770_q80.jpg",
                "price" => "3923"
            ],
            [
                "title" => "Normandie Court 225 E 95th St, New York, NY 10128 ",
                "description" => "
Normandie Court is a four tower property that occupies the entire block between Second and Third Avenues with our driveway entrance on 95th Street. In addition to 24-hour concierge and doormen, valet and package rooms, there is a roof-top health club, expanding over 25,000 sf, with spectacular views. The health club includes a pool, exercise rooms, steam and sauna rooms, sun decks, a lounge and many other amenities. There is a separate children’s playroom on the lobby level. On-site attended parking is available with direct access to the building. The neighborhood is very popular with young professionals as well as families. The services and amenities of the neighborhood make this an extremely desirable location. Note: We no longer offer the Moderate Income Program. Prices are subject to change on a daily basis. Lease terms are subject to change based on availability.
",
                "image" => "https://ar.rdcpix.com/1612308897/712fa47321add3f4e47ff4a603f37310c-f0xd-w1020_h770_q80.jpg",
                "price" => "3605"
            ]
        ];

        foreach ($data as $arr) {

            DB::table('listing')->insert([
                'title' => $arr['title'],
                'description' => $arr['description'],
                'image' => $arr['image'],
                'price' => $arr['price']
            ]);
        }
    }
}

php artisan db:seed

 

Now after the table is filled with dummy data let’s generate the authentication, listing model and controller, we will create two pages one that display a listing of items and the other display details of that item.



Generating Auth, Model and Controller

Run this command to generate the auth scaffolding

php artisan make:auth

This will generate the auth routes and the auth views, now let’s create a model and controller to display the listing data.

php artisan make:model Listing

php artisan make:controller ListingController

 

Now make the below modifications to the following files:

app/Listing.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Listing extends Model
{
    protected $table = "listing";
}

app/Http/Controllers/ListingController.php

<?php

namespace App\Http\Controllers;

use App\Listing;

class ListingController extends Controller
{
    public function getIndex()
    {
        $listings = Listing::all();

        return view('listing')->with('listings', $listings);
    }

    public function getDetails($id)
    {
        $listing = Listing::find($id);

        return view('details')->with('listing', $listing);
    }
}

resources/views/layouts/app.blade.php

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>Broadcasting</title>

    <!-- Fonts -->
    <link rel="dns-prefetch" href="https://fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet" type="text/css">

    <!-- Styles -->
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <nav class="navbar navbar-expand-md navbar-light navbar-laravel">
            <div class="container">
                <a class="navbar-brand" href="{{ url('/') }}">
                    {{ config('app.name', 'Laravel') }}
                </a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
                    <span class="navbar-toggler-icon"></span>
                </button>

                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <!-- Left Side Of Navbar -->
                    <ul class="navbar-nav mr-auto">

                    </ul>

                    <!-- Right Side Of Navbar -->
                    <ul class="navbar-nav ml-auto">
                        <!-- Authentication Links -->
                        @guest
                            <li class="nav-item">
                                <a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
                            </li>
                        @else
                            <li class="nav-item dropdown">
                                <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
                                    {{ Auth::user()->name }} <span class="caret"></span>
                                </a>

                                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                                    <a class="dropdown-item" href="{{ route('logout') }}"
                                       onclick="event.preventDefault();
                                                     document.getElementById('logout-form').submit();">
                                        {{ __('Logout') }}
                                    </a>

                                    <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                                        @csrf
                                    </form>
                                </div>
                            </li>
                        @endguest
                    </ul>
                </div>
            </div>
        </nav>

        <main class="py-4">
            @yield('content')
        </main>
    </div>
     @yield('scripts')
</body>
</html>

resources/views/listing.php

@extends('layouts.app')

@section('content')

    <div class="flex-center position-ref full-height">

        <div class="content">
            <h2 class="title m-b-md" style="background: #ccc">
                Listings
            </h2>

            <div class="row">
                @foreach($listings as $listing)
                    <article class="col-md-10">
                        <h4><a href="{{ url('details/' . $listing->id) }}">{{ $listing->title }}</a></h4>
                        <img src="{{ $listing->image }}" width="250" class="pull-left img-responsive thumb img-thumbnail" />
                        <p>
                            {{ substr($listing->description, 0, 50) }}
                        </p>
                    </article>
                    <hr/>
                @endforeach
            </div>
        </div>
    </div>

@stop

resources/views/details.php

@extends('layouts.app')

@section('content')

    <div class="flex-center position-ref full-height">

        <div class="content">
            <h1 class="title m-b-md" style="background: #ccc">
                {{ $listing->title }}
            </h1>
            <span><strong>Price: </strong> <i class="label label-success">$ {{ number_format($listing->price, 1) }}</i></span>
            <div class="row">
                <article class="col-md-12">
                    <img src="{{ $listing->image }}" class="img-responsive thumb img-thumbnail" />
                    <p>
                        {{ $listing->description }}
                    </p>
                </article>
            </div>
        </div>
    </div>

@stop

routes/web.php

<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', 'ListingController@getIndex');

Route::get('/details/{id}', 'ListingController@getDetails');

Auth::routes();

app/Http/Controllers/Auth/LoginController.php

.....
.....
.....

     protected $redirectTo = '/';

.....
.....
.....

app/Http/Controllers/Auth/RegisterController.php

.....
.....
.....

     protected $redirectTo = '/';

.....
.....
.....

Well done let’s move to the most important part of this post which is to setup laravel echo and redis

 



Installing dependencies

To prepare laravel echo server and socket io for use we will need to install some required dependencies:

Install npm dependencies

npm install

npm install laravel-echo

npm install socket.io-client

 

Install php redis

composer require predis/predis

 

Install laravel echo server with npm

npm install -g laravel-echo-server

Next open the .env file and change BROADCASR_DRIVER to redis

BROADCAST_DRIVER=redis

 

Preparing laravel echo server

Let’s prepare laravel echo server, to do this we will a config file for laravel-echo-server in our project root directory, fortunately laravel-echo-server can generate this file automatically for us using this command:

laravel-echo-server init

It will ask you a set of questions lets keep the defaults for now and be sure to select redis as the database. After it finishes it will generate a json file in your project root called “laravel-echo-server.json” like this one:

{
	"authHost": "http://localhost",
	"authEndpoint": "/broadcasting/auth",
	"clients": [],
	"database": "redis",
	"databaseConfig": {
		"redis": {},
		"sqlite": {
			"databasePath": "/database/laravel-echo-server.sqlite"
		}
	},
	"devMode": true,
	"host": null,
	"port": "6001",
	"protocol": "http",
	"socketio": {},
	"sslCertPath": "",
	"sslKeyPath": "",
	"sslCertChainPath": "",
	"sslPassphrase": "",
	"subscribers": {
		"http": true,
		"redis": true
	},
	"apiOriginAllow": {
		"allowCors": false,
		"allowOrigin": "",
		"allowMethods": "",
		"allowHeaders": ""
	}
}

Notice the port number, this port is used by the laravel echo server to listen to events, it’s important that no other process in your system takes this port. But we need away to make this port configurable so let’s add it to the .env

LARAVEL_ECHO_PORT=6001

Also add this as a javascript variable in main layout

resources/views/layouts/app.blade.php

<html>
.....
.....

<script>
        window.laravel_echo_port='{{env("LARAVEL_ECHO_PORT")}}';
</script>
</head>
.....
.....
</html>
   

 

Generating event

Let’s create a new event:

php artisan make:event ListingViewed

Open app/Events/ListingViewed.php and update as below:

<?php

namespace App\Events;

use App\Listing;
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Queue\SerializesModels;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class ListingViewed implements ShouldBroadcastNow
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $data = [];

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Listing $listing)
    {
        $this->data = [
            'listing' => $listing,
            'current_user' => Auth::user()->id
        ];
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new Channel('listing.' . $this->data['listing']->id);
    }
}

As you see above i have implemented the ShouldBroadcastNow interface and injected the listing model to event constructor, this data will be used when we listen for the event on the client side. Also i have specified that the event will broadcast on public channel with name “listing.{id}” on the method broadcastOn()

 

Firing the event

We need to fire the event when the user views a listing page and if he is authenticated so open app/Http/Controllers/ListingController.php and add this line:

.....
.....

public function getDetails($id)
    {
        $listing = Listing::find($id);

        if(Auth::check()) {
            event(new ListingViewed($listing)); // fire the event
        }

        return view('details')->with('listing', $listing);
    }
....
.....

 

Listening for the event

To listen for the event we will call the laravel echo javascript library we just installed above. To do add a new file in resources/assets/js to import laravel echo.

resources/assets/js/laravel-echo-setup.js

import Echo from 'laravel-echo';

window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: window.location.hostname + ":" + window.laravel_echo_port
});

As shown we imported laravel echo using Ecmascript6 syntax and then created a new object specifying that we use socket.io as the broadcaster and the host to listen to.

 

Next we will need to add this file to webpack.mix.js to be transpilled to Ecmascript5 

Open webpack.mix.js and add this line:

let mix = require('laravel-mix');

....
....
....

mix.js('resources/assets/js/laravel-echo-setup.js', 'public/js');

You can read more about webpack here.

Run this command in terminal to execute webpack scripts:

npm run dev

This will generate a javascript file in public/js/laravel-echo-setup.js , let’s add this script to our resources/views/details.blade.php

resources/views/details.blade.php

@extends('layouts.app')

@section('content')

    <div class="flex-center position-ref full-height">

        <div class="content">
            <h1 class="title m-b-md" style="background: #ccc">
                {{ $listing->title }}
            </h1>
            <span><strong>Price: </strong> <i class="label label-success">$ {{ number_format($listing->price, 1) }}</i></span>
            <div class="row">
                <article class="col-md-12">
                    <img src="{{ $listing->image }}" class="img-responsive thumb img-thumbnail" />
                    <p>
                        {{ $listing->description }}
                    </p>
                </article>
            </div>
        </div>
    </div>

@stop

@section('scripts')

    @if(\Auth::check())

        <script src="//{{ Request::getHost() }}:{{env('LARAVEL_ECHO_PORT')}}/socket.io/socket.io.js"></script>

        <script src="{{ url('/js/laravel-echo-setup.js') }}" type="text/javascript"></script>

        <script>
            window.Echo.channel('listing.{{$listing->id}}')
                .listen('ListingViewed', function (e) {

                    if(e.data.current_user != parseInt('{{ \Auth::user()->id }}')) {

                        showNotification("Another user looking at this listing right now");
                    }

                });

            function showNotification(msg) {

                if (!("Notification" in window)) {
                    alert("This browser does not support desktop notification");
                }

                else if (Notification.permission === "granted") {
                    // If it's okay let's create a notification
                    var notification = new Notification(msg);
                }

                else if (Notification.permission !== "denied") {
                    Notification.requestPermission().then(function (permission) {
                        // If the user accepts, let's create a notification
                        if (permission === "granted") {
                            var notification = new Notification(msg);
                        }
                    });
                }
            }
        </script>

    @endif

@stop

Here we updated our view by adding the socket.io script, note that this script is available only when the laravel-socket-server is turned on. Then called window.Echo.channel() method passing in the channel we need to listen to, this channel is same channel we registered the event above.

After that we use the listen() method, here we listened on “ListingViewed” event, this is the class name of the event we just created above.

I want to show notification to other users excluding the current user, so i add a condition like this:

if(e.data.current_user != parseInt('{{ \Auth::user()->id }}')) {

    showNotification("Another user looking at this listing right now");
}

Here i displayed browser notification for testing but you can use any other notification system as you like.



Testing notifications

To test the notifications create two user accounts and login from two different browsers, let’s say chrome and firefox and then in your project root directory run this command to turn on laravel echo server:

laravel-echo-server start

If it works successfully it should output

L A R A V E L  E C H O  S E R V E R

version 1.5.2

⚠ Starting server in DEV mode...

✔  Running at localhost on port 6001
✔  Channels are ready.
✔  Listening for http events...
✔  Listening for redis events...

Server ready!

Channel: listing.1
Event: App\Events\ListingViewed

Now try to access any listing in firefox with the first user account and then access the same listing on chrome with the second user account and it will show the notification like this.

laravel socket server notification testing

5 2 votes
Article Rating
Share this: