Building a Chat Room Using Pusher and Laravel Echo
In this tutorial, we’ll walk through the process of building a real-time chat room application using Pusher and Laravel Echo. We’ll cover everything from setup to advanced features like joining private chat rooms, managing active users, and more.
What is Pusher?
Pusher is a hosted service that provides real-time messaging via WebSockets, simplifying the process of implementing real-time features in applications. It allows clients (such as browsers or mobile apps) to subscribe to channels and receive updates when events occur.
What is Laravel Echo?
Laravel Echo is a JavaScript library that facilitates subscribing to channels and listening to events broadcast by Laravel. It abstracts the complexity of working with WebSockets and seamlessly integrates with various broadcasting drivers, including Pusher.
Prerequisites
Before we begin, ensure we have the following:
- Laravel installed on our system
- Basic knowledge of PHP and the Laravel framework
- Composer installed globally
- A Pusher account (sign up at Pusher if you haven’t already)
Implementation
1. Setting Up Laravel Project
Open the terminal and run the following command to create a new Laravel project:
composer create-project laravel/laravel laravel-chatroom
Once the project is created, navigate to the project directory, and run the following command to install Laravel:
composer install
Laravel uses broadcasting to handle WebSockets. Install the Pusher PHP SDK via Composer:
composer require pusher/pusher-php-server
By default, the broadcasting is not enabled in Laravel v11 applications. We may enable broadcasting using the install:broadcasting
Artisan command:
php artisan install:broadcasting
The install:broadcasting
command sets up two essential files for our application. It generates the config/broadcasting.php
configuration file. Additionally, it creates the routes/channels.php
file, allowing us to define and manage our application’s broadcast authorization routes and callbacks.
In our config/broadcasting.php
file, update the connections
array to include the Pusher connection:
'connections' => [
'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'),
'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => false,
],
// ...
Add the following environment variables to our .env
file:
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=YOUR_APP_ID
PUSHER_APP_KEY=YOUR_APP_KEY
PUSHER_APP_SECRET=YOUR_APP_SECRET
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=YOUR_APP_CLUSTER
Replace the placeholders with our actual Pusher app credentials.
In a private chat room, we want to ensure that only authorized users can access and participate in the conversation. This means we need to authenticate users and verify their identity before allowing them to join a chat room.
We will leverage Laravel’s built-in auth module, to handle user authentication and authorization. We can use the laravel/ui
package to generate the authentication scaffolding:
composer require laravel/ui
php artisan ui bootstrap --auth
2. Set Up the Database
Create a new database for our project, and update the DB_DATABASE
, DB_USERNAME
, and DB_PASSWORD
environment variables in our .env
file to match the database credentials.
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_cr
DB_USERNAME=root
DB_PASSWORD=
Step 3: Creating Models
First, let’s create a model for our ChatRoom
. This model will represent individual chat rooms where users can exchange messages.
Open the terminal and run the following Artisan command to generate the ChatRoom
model along with its migration file:
php artisan make:model ChatRoom -m
This command will create two files:
app/Models/ChatRoom.php
: This is the Eloquent model file where we’ll define ourChatRoom
model.database/migrations/YYYY_MM_DD_create_chat_rooms_table.php
: This is the migration file where we’ll define the database schema for thechat_rooms
table.
Open the migration file (database/migrations/YYYY_MM_DD_create_chat_rooms_table.php
) and define the schema for the chat_rooms
table. Here’s an example migration file:
public function up()
{
Schema::create('chat_rooms', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
Open app/Models/ChatRoom.php
and define the ChatRoom
model class:
protected $fillable = [
'name'
];
public function users()
{
return $this->belongsToMany(User::class, 'user_chat_rooms');
}
The users()
relationship method defined in the ChatRoom
model signifies a many-to-many relationship between ChatRoom
and User
models in Laravel’s Eloquent ORM.
Run the following Artisan command to generate the UserChatRoom
model along with its migration file:
php artisan make:model UserChatRoom -m
Update the UserChatRoom
model (app/Models/UserChatRoom.php
) with the following code:
protected $fillable = [
'user_id',
'chat_room_id',
];
Next, let’s create a model for our Message
. This model will represent individual messages sent within each chat room.
Run the following Artisan command to generate the Message
model along with its migration file:
php artisan make:model Message -m
Open the migration file (database/migrations/YYYY_MM_DD_create_messages_table.php
) and define the schema for the messages
table. Here’s an example migration file:
public function up(): void
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('chat_room_id');
$table->text('content');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('chat_room_id')->references('id')->on('chat_rooms')->onDelete('cascade');
});
}
In the example above, we have user_id
and chat_room_id
fields to associate messages with users and chat rooms.
Open app/Models/Message.php
and define the Message
model class:
class Message extends Model
{
protected $fillable = [
'user_id',
'chat_room_id',
'content',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function chatRoom()
{
return $this->belongsTo(ChatRoom::class);
}
}
In the Message
model, we have defined two relationships: user()
and chatRoom()
. Let’s explain each relationship in detail:
- The
user()
method defines a many-to-one relationship, indicating that eachMessage
belongs to a singleUser
. - The
chatRoom()
method also defines a many-to-one relationship, specifying that eachMessage
belongs to a singleChatRoom
.
Execute the migration command to create the messages
, chat_rooms
and user_chat_rooms
table in the database.
php artisan migrate
4. Create Events for Real-Time Messaging
Events in Laravel are essential for broadcasting data. Let’s create an event to broadcast new messages sent by users in a chat room.
php artisan make:event NewMessage
This class implements the ShouldBroadcast
interface, which indicates that this event should be broadcast to clients.
class NewMessage implements ShouldBroadcast
{
use SerializesModels;
public $message;
public function __construct(Message $message)
{
$this->message = $message;
}
public function broadcastOn()
{
return new PresenceChannel('chat-room.' . $this->message->chat_room_id);
}
public function broadcastWith()
{
return [
'id' => $this->message->id,
'content' => $this->message->content,
'user_id' => $this->message->user->id,
'user_name' => $this->message->user->name,
'created_at' => $this->message->created_at->toIso8601String(),
];
}
}
The constructor takes a Message
instance as an argument and assigns it to the $message
property.
The broadcastOn()
method specifies the channel(s) on which the event should be broadcasted.
A PresenceChannel
is a special type of channel that allows Laravel to keep track of the users who are currently subscribed to the channel. This is useful in scenarios where we need to know who is online or present in a particular channel.
In this specific case, the PresenceChannel
is used to broadcast the new message to all users who are currently subscribed to the chat room with the ID $this->message->chat_room_id
.
The broadcastWith()
method returns an array of data that should be broadcast to clients.
5. Implementing the ChatRoomController
Now that we’ve defined our event for broadcasting messages, we need to implement the ChatRoomController
to handle sending messages and displaying the chat room interface.
First, we create the ChatRoomController
using the Artisan command:
php artisan make:controller ChatRoomController
sendMessage
In the controller file, we create a sendMessage
method. This method will validate the request, create a new message, broadcast the message, and return a JSON response.
public function sendMessage(Request $request)
{
$validatedData = $request->validate([
'content' => 'required|string',
]);
// Get the authenticated user
$user = Auth::user();
// Create a new message instance
$message = new Message;
$message->user_id = $user->id;
$message->chat_room_id = $request->chat_room_id; // Assuming chat room ID is passed in the request
$message->content = $validatedData['content'];
$message->save();
// Broadcast the message to others in the chat room
broadcast(new NewMessage($message))->toOthers();
// Return a JSON response indicating success
return response()->json(['status' => 'Message sent!', 'message' => $message]);
}
This method handles the creation of a new message and broadcasting it to other users in the chat room. We retrieve the authenticated user using the Auth
facade and create a new Message instance that sets the user_id
, chat_room_id
, and content
properties, and saves the message to the database.
The method broadcasts the new message to all other users in the chat room using the broadcast
function and the NewMessage
event. The toOthers
method is used to exclude the authenticated user from receiving the broadcast.
index
The index
method is designed to handle displaying the chat room interface when a user navigates to a specific chat room. It also retrieves the message history for the chat room using the Message
model.
public function index(Request $request)
{
$roomId = $request->input('room_id');
$chatRoom = ChatRoom::findOrFail($roomId);
$messageHistory = Message::where('chat_room_id', $roomId)->get();
return view('chat.index', [
'messageHistory' => $messageHistory,
'chatRoom' => $chatRoom,
]);
}
6. Handling Joining and Managing Chat Rooms
In our chat application, users need to join a room before they can start chatting. We’ll implement functions to handle joining, creating, and leaving chat rooms. We’ll also leverage Laravel Echo and PresenceChannels to manage user presence within the chat rooms.
Users have two options for joining a chat room:
- The user selects an existing chat room to join.
- The user creates a new chat room and joins it.
public function joinRoom(Request $request)
{
$request->validate([
'room_name' => 'required|string|max:255',
]);
$chatRoom = ChatRoom::firstOrCreate(['name' => $request->room_name]);
if (!$chatRoom->users->contains(Auth::id())) {
$chatRoom->users()->attach(Auth::id());
}
return redirect()->route('chat.index', ['room_id' => $chatRoom->id]);
}
public function joinExistingRoom($id)
{
$chatRoom = ChatRoom::findOrFail($id);
if (!$chatRoom->users->contains(Auth::id())) {
$chatRoom->users()->attach(Auth::id());
}
return redirect()->route('chat.index', ['room_id' => $chatRoom->id]);
}
We need an API endpoint that allows users to leave a chat room. This endpoint will detach the user from the chat room.
public function leaveRoom(Request $request, $roomId)
{
$request->validate([
'roomId' => 'required|integer|exists:chat_rooms,id',
'userId' => 'required|integer|exists:users,id',
]);
$chatRoom = ChatRoom::findOrFail($request->input('roomId'));
$user = User::findOrFail($request->input('userId'));
if ($user->chatRooms->contains($chatRoom)) {
$user->chatRooms()->detach($chatRoom);
}
return response()->json(['status' => 'Left room successfully']);
}
We also add a method to list all available chat rooms. We used withCount('users')
to count the number of users in each chat room including that count and returning a view.
public function showJoinRoomForm()
{
$chatRooms = ChatRoom::withCount('users')->get();
return view('chat.join', ['chatRooms' => $chatRooms]);
}
7. Routing
Ensure we have the necessary routes defined in our web.php
file:
use App\Http\Controllers\ChatRoomController;
Route::get('/chat', [ChatRoomController::class, 'index'])->name('chat.index');
Route::get('/chat/rooms', [ChatRoomController::class, 'listRooms'])->name('chat.rooms');
Route::post('/chat/join', [ChatRoomController::class, 'joinRoom'])->name('chat.joinRoom');
Route::get('/chat/join/{id}', [ChatRoomController::class, 'joinExistingRoom'])->name('chat.joinExistingRoom');
Route::post('/chat/leave', [ChatRoomController::class, 'leaveRoom'])->name('chat.leaveRoom');
Route::post('/chat/send', [ChatRoomController::class, 'sendMessage'])->name('chat.sendMessage');
Route::get('/chat/join', [ChatRoomController::class, 'showJoinRoomForm'])->name('chat.showJoinRoomForm');
In Laravel, the routes/channels.php
file is used to define the authorization logic for broadcasting events over channels. In this case, we’re defining a channel named chat-room.{roomId}
.
The closure function passed to Broadcast::channel
is called an “authorization callback“. When using a PresenceChannel
, this authorization callback is used to authenticate users and authorize them to join the channel.
In the context of a chat room, this means that only authorized users will be able to join the channel and receive broadcasts from other users in the same room.
Broadcast::channel('chat-room.{roomId}', function ($user, $roomId) {
if (Auth::check()) {
$chatRoom = ChatRoom::find($roomId);
if ($chatRoom && $chatRoom->users()->where('user_id', $user->id)->exists()) {
return ['user' => $user, 'roomId' => $roomId];
}
}
return false;
});
When a user joins the channel, Laravel will pass this array as part of the channel’s payload, allowing us to access the user and room ID information in the JavaScript code.
8. Frontend Development
Now we are ready to design our front page. We’ll be creating two pages:
- Join Room Page: Displays the list of chat rooms and provides forms for joining or creating rooms.
- Chatting Page: Chat interface where users can see the message history and send new messages.
Now, let’s create a new blade template file resources/views/chat/rooms.blade.php
:
@extends('layouts.app')
@section('content')
<div class="container py-4">
<div class="row">
<div class="col-md-8">
<!-- Left Column: Active Rooms -->
<div class="card mb-4">
<div class="card-body">
<h2 class="card-title mb-4">Active Chat Rooms</h2>
<ul class="list-group">
@foreach($chatRooms as $room)
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<span class="font-weight-bold">{{ $room->name }}</span>
<span class="text-muted">({{ $room->users_count }} users)</span>
</div>
<form method="GET" action="{{ route('chat.joinExistingRoom', ['id' => $room->id]) }}">
@csrf
<button type="submit" class="btn btn-outline-primary">
<i class="fa-solid fa-right-to-bracket"></i>
</button>
</form>
</li>
@endforeach
</ul>
</div>
</div>
</div>
<div class="col-md-4">
<!-- Right Column: Join Form -->
<div class="card">
<div class="card-body">
<h2 class="card-title mb-4">Create a Chat Room</h2>
<form method="POST" action="{{ route('chat.joinRoom') }}">
@csrf
<div class="form-group mb-3">
<label for="room_name">Room Name:</label>
<input id="room_name" type="text" class="form-control @error('room_name') is-invalid @enderror" name="room_name" required autofocus>
@error('room_name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<button type="submit" class="btn btn-primary">
<i class="fa-solid fa-circle-plus"></i> Create Room
</button>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
Next, we create a new blade template file resources/views/chat/index.blade.php
:
@extends('layouts.app')
@section('content')
<style>
....
</style>
<script>
....
</script>
<div class="py-4 bg-light">
<div class="container">
<input type="hidden" id="chatRoomId" value="{{ $chatRoom->id }}">
<div class="row g-4">
<!-- Active Users Section -->
<div class="col-md-4">
<div class="card">
<div class="card-body border-bottom">
<h3 class="card-title">{{ $chatRoom->name }}</h3>
</div>
<ul class="list-group list-group-flush active-users">
</ul>
</div>
</div>
<!-- Message History Section -->
<div class="col-md-8">
<div class="card">
<div class="card-body overflow-auto message-history" style="height: 400px;">
@if ($errors->any())
<div class="alert alert-danger">
<strong>Whoops!</strong>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@foreach($messageHistory as $message)
<div class="message mb-2 d-flex flex-column align-items-{{ $message->user_id === Auth::id() ? 'end' : 'start' }}">
<div class="message-content-wrapper @if($message->user_id === Auth::id()) bg-primary text-white @else bg-light @endif">
@if($message->user_id !== Auth::id())
<strong class="text-primary">
{{ $message->user->name }}:
</strong>
@endif
<div class="message-content">
<span>{{ $message->content }}</span>
</div>
</div>
</div>
@endforeach
</div>
<!-- Message Sending Section -->
<form id="messageForm" method="POST" class="card-body border-top">
@csrf
<input type="hidden" name="chat_room_id" value="{{ $chatRoom->id }}">
<div class="mb-3">
<textarea name="content" class="form-control" rows="3" placeholder="Type your message..."></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">Send</button>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
9. Listening to Events on the Channel
We use Laravel Echo to listen to events and handle user presence on the client side. We need to install Laravel Echo and Pusher JS:
npm install --save laravel-echo pusher-js
In the resources/js/boostrap.js
JavaScript file, set up Echo:
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Pusher.logToConsole = true;
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'YOUR_KEY',
encrypted: true,
cluster: 'YOUR_CLUSTER',
forceTLS: false
});
Next, we need to subscribe to the chat-room
channel and listens to the NewMessage
event in our resources/views/indx.blade.php
:
<script>
function joinRoom(roomId) {
const channel = window.Echo.join('chat-room.' + roomId);
channel
.here((users) => {
console.log('Current users:', users);
updateActiveUsers(users);
})
.joining((user) => {
console.log('User joined:', user);
addActiveUser(user);
})
.leaving((user) => {
console.log('User left:', user);
removeActiveUser(user);
})
.listen('NewMessage', (e) => {
console.log('New message received:', e.content);
document.querySelector('.message-history').innerHTML += `<div class="message mb-2 d-flex flex-column align-items-start">
<div class="message-content-wrapper bg-light">
<strong class="text-primary">${e.user_name}:</strong>
<div class="message-content">
<span>${e.content}</span>
</div>
</div>
</div>`;
});
}
function updateActiveUsers(users) {
const userList = document.querySelector('.active-users');
userList.innerHTML = '';
users.forEach(user => {
userList.innerHTML += `
<li class="d-flex align-items-center mb-2">
<i class="fa fa-circle text-success me-2"></i>
<span>${user.user.name}</span>
</li>`;
});
}
function addActiveUser(user) {
const userList = document.querySelector('.active-users');
userList.innerHTML += `
<li class="d-flex align-items-center mb-2">
<i class="fa fa-circle text-success me-2"></i>
<span>${user.user.name}</span>
</li>`;
}
function removeActiveUser(user) {
const userList = document.querySelector('.active-users');
userList.querySelectorAll('li').forEach(li => {
const userName = li.querySelector('span').textContent.trim();
if (userName === user.user.name) {
li.remove();
axios.post(`/chat/leave`, {
roomId: user.roomId,
userId: user.user.id
},{
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
})
.then(response => {
console.log('Status:', response.data.status);
})
.catch(error => {
console.error('Error:', error);
});
}
});
}
document.addEventListener('DOMContentLoaded', function() {
const roomId = document.getElementById('chatRoomId').value;
joinRoom(roomId);
});
</script>
The joinRoom
function is responsible for joining a specific chat room channel using Laravel Echo and Pusher. It sets up event listeners to handle updates to the list of active users and incoming messages.
Follow by we created a few more functions updateActiveUsers
, addActiveUser
and removeActiveUser
to manage the users in the chat room.
10. Running and Testing the Application
Now that we have everything set up, we are ready to test our chat room application. First run the following command to compile our assets:
npm run build
Run the following command to start the Laravel development server:
php artisan serve
To test the real-time functionality, we’ll need to register at least two users. We can do this through the built-in registration page provided by Laravel. Open the browser and navigate to http://localhost:8000/register
to register a few users.
After registering, log in using the newly created account. Navigate to http://localhost:8000/chat/rooms
to join a chat rooms first.
Conclusion
In this tutorial, we walked through the process of building a real-time chat room application using Pusher and Laravel Echo. This setup provides a dynamic and engaging user experience, demonstrating how modern web technologies can be utilized to create interactive and responsive applications. The full source code is available on GitHub.
Share this content:
Leave a Comment