Building a real-time chat demo app with Laravel WebSockets (Part 1) cover image

Building a real-time chat demo app with Laravel WebSockets (Part 1)

Tom Oehlrich

Thanks to services like like Pusher.com, Firebase and many others, implementing realtime functionality has become a breeze. But what if data privacy is a big concern and your European company prefers its data to only touch a server inside the EU? Or maybe you just want complete control over the implementation and workflow.

You could do a lot of polling on the client side. But that becomes unstable pretty fast. So running your own WebSocket server can be an attractive alternative. Obviously the downside of that is that now your company is in charge of maintenance and of keeping the service running. Services like Pusher.com on the other hand are specialized on exactly that.

But in this article we will see how easy it is to run your own WebSocket server with PHP only. And Javascript on the frontend of course. To be specific we are going to use Laravel and VueJs.

We will use the amazing package laravel-websockets by Marcel Pociot and Freek Van der Herten which lets us run our own WebSocket server by taking away a lot of the "pain" to set one up.

This package has multi-tenancy support by default. So basically you have two options:

  1. Make a standalone-installation of the WebSocket server and use the same installation for different projects/apps that you want to develop.
  2. Install this package into an existing Laravel project and specifically use it only for that project.

We opt for option number 1 and separate the WebSocket server and our web project. For our demo we will develop a very simple chat app which kind of makes sense to showcase realtime functionality.

So this is what we are going to do:

In part 1 (this post) we are setting up the websocket server application and provide an API endpoint for our client chat app.

In part 2 we are setting up the client chat app using Laravel and Vue.js and use Laravel Echo as a wrapper for the Javascript implementation.

Part 3 will deep dive a little bit into a couple of configurations that worked for me in my local development environment and on a DigitalOcean droplet.


So let's start by creating a new Laravel app for the Laravel WebSocket server.

laravel new websocket-server

Next we set up a database and add the credentials into our .env file.

The WebSocket server package comes with a statistics dashboard which by default is only accessible in local environment. To access the dashboard in production enviroment we will have to make a small change later. Also we will add some login functionality to protect the dashboard from non-authorized view.

composer require laravel/ui

php artisan ui vue --auth

Time to compile our frontend scaffolding:

npm install && npm run dev

Since we don't need any registration functionality we can just replace

Auth::routes();

with

Auth::routes(['register' => false]);

in our routes/web.php file.

We need a user to log in with.
So let's run our migrations to generate the users table.

php artisan migrate

If you encounter a "specified key was too long" error just make some minor adjustments to your AppServiceProvider.php file and add

Schema::defaultStringLength(191); 

to the boot method and don't forget to add

use Illuminate\Support\Facades\Schema; 

to the head of the file.

I am doing a quick php artisan tinker to create a user.

$user = new App\User
$user->name = 'admin'
$user->email = '<EMAIL>'
$user->password = Hash::make('<PASSWORD>')
$user->save();

It's time to install the laravel-websockets package.

composer require beyondcode/laravel-websockets

The migration file for the statictics database table can be generated with

php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider"
    --tag="migrations"

Finally we have to run our migrations again

php artisan migrate

We also need the package's config file. So let's generate that:

php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider"
    --tag="config"

The most important part in the config file is the apps array. As already mentioned the laravel-websockets package comes with multi-tenancy functionality out of the box. So this is the place to add as many apps as you need.

Although we are not using the pusher.com service we have to set some values for

in the .env file.

This is because the laravel-websockets package is fully compatible with the Pusher API and we can make use of it. So what we are basically doing is using fake IDs. We just have to be concise with it across our project.

This is how my apps array looks for now. I have added some more environment variables to be more flexible.

'apps' => [
    [
        'id' => env('PUSHER_APP_ID'),
        'name' => env('PUSHER_APP_NAME'),
        'key' => env('PUSHER_APP_KEY'),
        'secret' => env('PUSHER_APP_SECRET'),
        'path' => env('PUSHER_APP_PATH'),
        'capacity' => null,
        'enable_client_messages' => false,
        'enable_statistics' => env('PUSHER_APP_ENABLE_STATICTICS'),
    ],
],

The statistics dashboard of the Websocket server should be available under [YOUR DOMAIN]/laravel-websockets.
You can change this path in config/websockets.php.
As mentioned before the dashboard by default is only reachable in local environment. Here you can read more about it and how to change it.

In config/websockets.php we also add the 'auth:web' middleware to the middleware array to protect the dashboard.

...
'middleware' => [
    'web',
    Authorize::class,
    'auth:web'
],
...

I have added another environment variable in .env

LARAVEL_WEBSOCKETS_PERFORM_DNS_LOOKUP=true

and use it in config/websockets.php

'perform_dns_lookup' => env('LARAVEL_WEBSOCKETS_PERFORM_DNS_LOOKUP', true),

in order to have the websockets package write statistics into the database.


We need the official Pusher PHP SDK but this package should be installed already while installing the websocket server package. If not just do

composer require pusher/pusher-php-server "~4.0"

Since we want to broadcast events from Laravel by using the Pusher API we have to change

BROADCAST_DRIVER=log

to

BROADCAST_DRIVER=pusher

in .env.

Naturally the pusher SDK would assume to broadcast to the pusher.com server. But we are not using that. So we have to make some more modifications in "options" in the pusher connection config in config/broadcasting.php.

...
'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' => env('PUSHER_APP_ENCRYPTED'),
        'host' => env('PUSHER_APP_HOST'),
        'port' => env('PUSHER_APP_PORT'),
        'scheme' => env('PUSHER_APP_SCHEME')
    ],
],
...

Again I have added more environment variables.

PUSHER_APP_ENCRYPTED=false
PUSHER_APP_HOST="127.0.0.1"
PUSHER_APP_PORT=6001
PUSHER_APP_SCHEME="http"

Next it's time to create an event to broadcast.

php artisan make:event NewChatMessage

This is how the NewChatMessage event looks like

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;


class NewChatMessage implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $message;
    public $user;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct($message, $user)
    {
        $this->message = $message;
        $this->user = $user;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new Channel('chat');
    }
}

Our event accepts a message and a user/author and implements ShouldBroadcast.

We are using Channel as opposed to PrivateChannel for our demo. In a real-world application your users would probably have to login into their chat app and you likely would use private channels.

Finally it's time to start our websocket server

php artisan websocket:serve

Now in our dashboard under [YOUR DOMAIN]/laravel-websockets we can select the client app and press Connect.


The last thing we need (websocket-)server-side is an API endpoint where our Laravel client chat app can send chat messages to.

Add a route to routes/api.php

Route::post('/message', 'MessageController@broadcast');

and create a controller

php artisan make:controller MessageController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Events\NewChatMessage;

class MessageController extends Controller
{
    public function broadcast(Request $request) {

        if (! $request->filled('message')) {
            return response()->json([
                'message' => 'No message to send'
            ], 422);
        }

        // TODO: Sanitize input

        event(new NewChatMessage($request->message, $request->user));

        return response()->json([], 200);

    }
}

For quick tests of HTTP endpoints I like using a Visual Studio extension called REST client.

Just create a file test.http in your root folder and tell Git to ignore it in .gitignore if you like.

test.http could look like this:

POST [YOUR DOMAIN]/api/message
Content-Type: application/json
Accept: application/json

{
"message": "Test message",
"user": "Testuser"
}

If the Send request link does not show up add a couple of empty lines at the top of the file.

Sending the test request should now trigger our event and be broadcast to our websocket server.
It should popup in the Events section of [YOUR DOMAIN]/laravel-websockets.

That's it server-side.
Let's proceed with part 2 of this article.

You can find the full source code in this GitHub repo.