Create a Chat with Laravel and Pusher
Tags: laravel
Published: 2024-03-21 18:22:00

In the digital age, real-time web applications have transformed how we interact with technology and each other. From live chat systems to instant content updates, users now expect seamless, instantaneous communication at their fingertips. This tutorial will introduce you to the exciting world of real-time web applications using Laravel and Pusher, two powerful tools that make implementing real-time features a breeze.

##Install and run  migrations

First let us install the laravel.

```
composer create-project laravel/laravel .
```

For the ease of use let's use SQLite database. In .env change nysql to sqlite here. And remove `DB_DATABASE=laravel` in .env
(And make sure to comment this - DB_DATABASE=laravel if you use sqlite)

```
DB_CONNECTION=sqlite
```

Now let's write migrations.

```
php artisan make:migration create_chat_messages_table --create=chat_messages
```

Now add this to migration table

```
public function up(): void
    {
        Schema::create('chat_messages', function (Blueprint $table) {
            $table->id();
            $table->string('user')->nullable();
            $table->text('message_text')->nullable();
            $table->timestamps();
        });
 }
```

now run 

```
php artisan migrate
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gg4zr0iif6yp3t0ymiaw.png)

##Pusher
First log into pusher and create an app. 

https://dashboard.pusher.com/apps

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/auq1k0b7r5fl4t5e9he5.png)

After creating the app get the these fields and save in side .env

```
BROADCAST_DRIVER=pusher
...

PUSHER_APP_ID=""
PUSHER_APP_KEY=""
PUSHER_APP_SECRET=""
PUSHER_APP_CLUSTER=
```
install pusher add this inside command line

```
composer require pusher/pusher-php-server
```


##Vue JS

Now let's build the UI with VusJS.

```
npm install
npm install vue@latest vue-loader@latest
npm i @vitejs/plugin-vue
```

###Edit File vite.config.js

```
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue'

export default defineConfig({
    plugins: [
        vue(),
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
});
```

Now got to `resources\js\app.js` and add these lines.

```
import { createApp } from 'vue';
import Chat from './components/Chat.vue';
import App from './App.vue'
const app = createApp(App);
app.component('Chat', Chat);

app.mount("#app");
```

##Let's start with UI

And create this file `resources\js\components\Chat.vue`

Add these lines

```
<template>
    <div class="chat-box">
        <div class="chat-box-header">Chat</div>
        <div class="chat-box-messages" id="chat-messages">
            <div class="message other-user">Hi, how are you?</div>
            <div class="message current-user">I'm good, thanks! And you?</div>
        </div>
        <div class="chat-box-input">
            <input type="text" class="input_border" placeholder="Type a message..." />
            <button type="button">Send</button>
        </div>
    </div>
</template>


<style scoped>

.chat-box-input {
    padding: 10px;
    background-color: #fff;
    border-top: 1px solid #eee;
    display: flex; /* Aligns input and button side by side */
}

.chat-box-input input {
    flex-grow: 1; /* Allows input to take up available space */
    margin-right: 8px; /* Adds spacing between input and button */
    padding: 8px 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box;
}

.chat-box-input button {
    padding: 10px 15px;
    background-color: #007bff;
    color: #ffffff;
    border: none;
    border-radius: 4px;
    cursor: pointer; /* Changes cursor to pointer on hover */
    white-space: nowrap; /* Prevents wrapping of text in the button */
}

.chat-box-input button:hover {
    background-color: #0056b3; /* Darker shade on hover for visual feedback */
}

.chat-box {
    display: flex;
    flex-direction: column;
    max-width: 320px;
    min-width: 300px;
    height: 500px;
    border: 1px solid #ccc;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    font-family: Arial, sans-serif;
    background-color: #fff;
}

.chat-box-header {
    background-color: #007bff;
    color: #ffffff;
    padding: 10px;
    text-align: center;
    font-size: 16px;
}

.chat-box-messages {
    flex: 1;
    padding: 10px;
    overflow-y: auto;
    background-color: #f9f9f9;
    display: flex;
    flex-direction: column;
}

.message {
    margin-bottom: 12px;
    padding: 8px 10px;
    border-radius: 20px;
    display: inline-block;
    max-width: 70%;
}

.current-user {
    background-color: #007bff;
    color: #ffffff;
    margin-left: auto;
    text-align: right;
    align-self: flex-end;
}

.other-user {
    background-color: #e9ecef;
    color: #333;
}

.chat-box-input {
    padding: 10px;
    background-color: #fff;
    border-top: 1px solid #eee;
}

.chat-box-input input {
    width: 100%;
    padding: 8px 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box;
}

.input_border{
    border: 1px solid #ccc;
}
</style>
```

Now go to this file `resources\views\welcome.blade.php` delete everything and add these,

```
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" content="{{ csrf_token() }}">

        <title>Laravel</title>
        @vite('resources/js/app.js')
    </head>
    <body class="antialiased">
        <div id="app"></div>
    </body>
</html>
```

Add this file `resources\js\App.vue`

Fill with these content,

```
<template>
    <chat></chat>
</template>
```

Run this command in one console (in your project directory)

```
php artisan serve
```

Run this command in another console (in your project directory)

```
npm run dev
```

Now you will see this fine UI in localhost:8000


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mcnq1iwi73hgipoy45cm.png)

Now let's create a model and event.

```
php artisan make:model ChatMessage
php artisan make:event MessageCreated
```

This is ChatMessage Model

```
class ChatMessage extends Model
{
    use HasFactory;

    protected $fillable = ['user', 'message_text'];
}
```

Then 'MessageCreated' event.

```
<?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 MessageCreated implements ShouldBroadcast
{
    
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $chat;

    /**
     * Create a new event instance.
     */
    public function __construct($chat)
    {
        $this->chat = $chat;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array<int, \Illuminate\Broadcasting\Channel>
     */
    public function broadcastOn(): array
    {
        return [
            new Channel('chats'),
        ];
    }
}
```

Check here we have added `ShouldBroadcast` broadcasting on a private channel. Broadcasting on public channel means when use echo anyone will see that.

And need to enable providers too

**config\app.php**


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a850ch773e04psikhxkm.png)



And now the routes.

```
Route::post('/chat', function () {
    $chat = ChatMessage::create([
        'message_text' => request('message_text') 
    ]);

    event((new MessageCreated($chat))->dontBroadcastToCurrentUser());
});
```

What this does is saving `ChatMessage` inside table and send it to pusher. 

And I changed the `resources\js\components\Chat.vue` also. Keep the `<style>` tag and replace all other code with this

```
<template>
    <div class="chat-box">
        <div class="chat-box-header">Chat</div>
        <div class="chat-box-messages" id="chat-messages">
            <div v-for="message in messages" :key="message.message_text" 
                :class="message.user == username ? 'message current-user' : 'message other-user'">
                {{ message.message_text }}
            </div>

        </div>
        <div class="chat-box-input">
            <input type="text" v-model="newMessage" class="input_border" placeholder="Type a message..." />
            <button type="button" @click="setMessage">Send</button>
        </div>
    </div>
</template>

<script setup>
    import { onMounted, ref } from 'vue';

    const messages = ref([]);
    const newMessage = ref('');

    

    const setMessage = () => {
        axios.post('/chat', {
            message_text: newMessage.value
        }).then(() => {
            let message = {
                message_text: newMessage.value
            }
            messages.value.push(message);
            newMessage.value = ""; 
        });
    }

</script>
```

What this does is when you click **Send** button send call the `Route::post('/chat', ` route. And there inside the event call they send API call to the pusher.

Now if you check in ui by clicking the button 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2q9qp17ute5j8h8ivups.png)

You will see this in pusher dashboard.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/or5wfultcm2afdo31ltk.png)

But you may notice that after pressing the button it takes long time to populate the message in screen. That happens it takes long time to send pusher by backend. We can make this call asynchronous.

This is how we does that,

###Step 1: Configure the Queue Driver
First, update your .env file to use the database queue driver:

```
QUEUE_CONNECTION=database
```

This setting tells Laravel to use the database for queueing jobs.

###Step 2: Create the Queue Table
Laravel needs a table in your database to store queued jobs. You can create this table by running the queue table migration that comes with Laravel. In your terminal, execute:

```
php artisan queue:table
```

Then, apply the migration to create the table:

```
php artisan migrate
```

###Step 4: Ensure Event Listeners Implement ShouldQueue

```
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MessageCreated implements ShouldBroadcast, ShouldQueue
{
    
```

###Step 5: Running the Queue Worker

```
php artisan queue:work

```

This command starts a queue worker that listens for new jobs on the database queue and processes them. Keep this worker running to ensure your queued jobs are processed.

Now you see significant speed increase in API call


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zukuezq7pivjk546nmzk.png)

I have added few changes to chat interface. You can access full example in this git repo..

https://github.com/vimuths123/chatapp

Run this in cli

```
npm install --save laravel-echo pusher-js
```

And you need to add this to `resources\js\bootstrap.js`

```
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;


window.Echo = new Echo({
  broadcaster: 'pusher',
  key: 'pubkey',
  cluster: 'mt1',
  forceTLS: true
});
```

And don't forget to replace **pubkey ** with your own pusher key

Change the routes file (routes\web.php) to this.

```
Route::post('/chat', function () {
    $chat = ChatMessage::create([
        'user' => request('user'), 
        'message_text' => request('message_text') 
    ]);

    event((new MessageCreated($chat))->dontBroadcastToCurrentUser());
});
```

And replace **<template> and <script>** sections of `resources\js\components\Chat.vue` file to this


```
<template>
    <div v-if="!username">
        <input type="text" v-model="tempUsername" placeholder="Enter your username">
        <button @click="setUsername">Join Chat</button>
    </div>
    <div class="chat-box" v-else>
        <div class="chat-box-header">Chat</div>
        <div class="chat-box-messages" id="chat-messages">
            <div v-for="message in messages" :key="message.message_text" 
                :class="message.user == username ? 'message current-user' : 'message other-user'">
                {{ message.message_text }}
            </div>

        </div>
        <div class="chat-box-input">
            <input type="text" v-model="newMessage" class="input_border" placeholder="Type a message..." />
            <button type="button" @click="setMessage">Send</button>
        </div>
    </div>
</template>

<script setup>
    import { onMounted, ref } from 'vue';

    const username = ref('');
    const tempUsername = ref('');
    const messages = ref([]);
    const newMessage = ref('');

    const setUsername = () => {
        username.value = tempUsername.value.trim();
        tempUsername.value = ''; 
    };

    const setMessage = () => {
        axios.post('/chat', {
            user: username.value,
            message_text: newMessage.value
        }).then(() => {
            let message = {
                user: username.value,
                message_text: newMessage.value
            }
            messages.value.push(message);
            newMessage.value = ""; 
        });
    }

     onMounted(async () => {
        
        window.Echo.channel('chats').listen('MessageCreated', (e) => {
            let message = {
                user: e.chat.user,
                message_text: e.chat.message_text
            }

            messages.value.push(message);
        });
     });

</script>
```


Here we have added the two users and added `window.Echo.channel('chats').listen('MessageCreated', (e) => {`
So always code listens to **chats** channel.

And also there is a little performance optimization we can do for improve this. We could use redis to store these jobs instead of database. 

Redis operates in memory, offering much faster read and write operations compared to disk-based systems. This is crucial for chat applications where timely processing of messages and notifications is critical for user experience.

By using Redis for queue management and temporary data storage (e.g., unread messages or active users), you can significantly reduce the load on your main database, reserving it for more critical tasks like persisting chat logs or user information. 

Let's check how to use redis for job queues

First install predis

```
composer require predis/predis
```

Then let's tell laravel to use redis for queue. Add this in .env

```
REDIS_CLIENT=predis
QUEUE_CONNECTION=redis
```

And don't forget to **install redis on your system** before