DEV Community

Cover image for Create a Chat with Laravel and Pusher
vimuth
vimuth

Posted on • Updated on

Create a Chat with Laravel and Pusher

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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now let's write migrations.

php artisan make:migration create_chat_messages_table --create=chat_messages
Enter fullscreen mode Exit fullscreen mode

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();
        });
 }
Enter fullscreen mode Exit fullscreen mode

now run

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

Image description

Pusher

First log into pusher and create an app.

https://dashboard.pusher.com/apps

Image description

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=
Enter fullscreen mode Exit fullscreen mode

install pusher add this inside command line

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

Vue JS

Now let's build the UI with VusJS.

npm install
npm install vue@latest vue-loader@latest
npm i @vitejs/plugin-vue
Enter fullscreen mode Exit fullscreen mode

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,
        }),
    ],
});
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Add this file resources\js\App.vue

Fill with these content,

<template>
    <chat></chat>
</template>
Enter fullscreen mode Exit fullscreen mode

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

php artisan serve
Enter fullscreen mode Exit fullscreen mode

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

npm run dev
Enter fullscreen mode Exit fullscreen mode

Now you will see this fine UI in localhost:8000

Image description

Now let's create a model and event.

php artisan make:model ChatMessage
php artisan make:event MessageCreated
Enter fullscreen mode Exit fullscreen mode

This is ChatMessage Model

class ChatMessage extends Model
{
    use HasFactory;

    protected $fillable = ['user', 'message_text'];
}
Enter fullscreen mode Exit fullscreen mode

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'),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

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

And now the routes.

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

    event((new MessageCreated($chat))->dontBroadcastToCurrentUser());
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

You will see this in pusher dashboard.

Image description

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then, apply the migration to create the table:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

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
{

Enter fullscreen mode Exit fullscreen mode

Step 5: Running the Queue Worker

php artisan queue:work

Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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());
});
Enter fullscreen mode Exit fullscreen mode

And replace and 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) => {
            alert('fff')
            let message = {
                user: e.chat.user,
                message_text: e.chat.message_text
            }

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

</script>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

REDIS_CLIENT=predis
QUEUE_CONNECTION=redis
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)