Build a chat app with Vue.js and Laravel
Familiarity with JavaScript and PHP would be beneficial, but isn’t compulsory to complete this tutorial as I will also show you how to setup Laravel from scratch.
What you will build
In this tutorial you will do the following:
- Create a Pusher account.
- Set Laravel broadcasting to use Pusher Channels.
- Connect a Vue.js client to Channels with Laravel Echo.
- A simple form to send messages.
- Display received messages in a list.
The code of the completed demo is available on GitHub.
1. Getting started
Before we dive into the tutorial there are a few steps you need to take to get set up.
Pusher account
If you don’t already have one you should go ahead and register for a Pusher account. The Channels sandbox plan is completely free and will be more than ample to complete this sample project.
How to create a Channels app
Once you have signed up for a Pusher account you need to create a Channels app by clicking “Get Started” under Channels:
Follow the steps in the dialog box to name your app and choose a preferred cluster location from the list. The tech stack settings are optional, they are just for the getting started page.
From the App Keys page note down the app_id
, key
, secret
and cluster
as you will need these to connect to your Channels app.
Set up a Laravel environment
For this tutorial, you will need PHP 8, Composer 2 and Node.js 16. For MacOS I recommend installing them with Homebrew. For Windows see instructions for PHP, Composer and Node.
2. Creating the project
We’re going to use Composer’s create-project command to download and install the latest starter Laravel project into a “laravel-chat” folder. Our pusher-http-php library supports a minimum Laravel version of 8.29.
composer create-project laravel/laravel laravel-chat
You can take a look at this now:
cd laravel-chat
php artisan serve
Go to http://127.0.0.1:8000/ in your web browser and you should see a Laravel starter welcome page.
3. Set up Pusher
Switch the broadcast driver to Pusher
We need to tell Laravel to use Pusher Channels for realtime updates. Register the Broadcast application service by opening config/app.php
and uncommenting this line:
// App\Providers\BroadcastServiceProvider::class,
In your project root’s .env
file, change the broadcast driver from its default “log” value to “pusher.
// .env
BROADCAST_DRIVER=pusher
If you scroll further down this file, you can see some of the Pusher environment variables are already there. Laravel already integrates some of the Pusher configuration, so you just need to fill in keys you got when you created your Channels app.
PUSHER_APP_ID=123456
PUSHER_APP_KEY=192b754680b23ce923d6
PUSHER_APP_SECRET=a64521345r457641sb65pw
PUSHER_APP_CLUSTER=mt1
Install the Channels SDK
Though Laravel already has some Pusher integration, we still need to install the Pusher PHP SDK:
composer require pusher/pusher-php-server
Install front-end dependencies
We will need Laravel Echo, which is the javascript library for listening to broadcast events, and pusher-js for connecting your client to Pusher. Install them by navigating into the project directory and running this npm command (which is javascript package manager installed as part of Node):
npm install --save laravel-echo pusher-js
Now you can enable your Pusher credentials in your Echo client. Uncomment these statements at the bottom of resources/js/bootstrap.js
.
\\ import Echo from 'laravel-echo';
\\ window.Pusher = require('pusher-js');
\\ window.Echo = new Echo({
\\ broadcaster: 'pusher',
\\ key: process.env.MIX_PUSHER_APP_KEY,
\\ cluster: process.env.MIX_PUSHER_APP_CLUSTER,
\\ forceTLS: true
\\ });
And thats it. The public key and cluster is already taken from your .env file that we previously updated.
4. Add the login system
Add the login pages
Let’s add the login pages. In the terminal, install the laravel/ui package:
composer require laravel/ui
Then the following command will enable the Vue javascript framework and add a login page to your project:
php artisan ui vue --auth
Finally install the dependencies and compile it with the below command:
npm install && npm run dev
Take a look at your new login page by starting your webserver:
php artisan serve
Then go to http://127.0.0.1:8000/ in your web browser. On the Laravel welcome screen, you will see a new login and register button on the top right.
Create the database
Before we can register a user, we will need to connect to a database for Laravel to use. Create an empty sqlite file with
touch database/db.sqlite
and in your .env
, replace this:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
with this:
DB_CONNECTION=sqlite
DB_DATABASE=<Full path to db.sqlite file>
Make sure you replace <Full path to db.sqlite file>
with the actual path to the file, for example /Users/user/laravel-chat/database/db.sqlite
.
Right now db.sqlite
is just an empty file. We will first need to insert Laravel’s default users
table (and a few other user account related tables) with the following command:
php artisan migrate
In your terminal, run php artisan serve
, navigate to http://127.0.0.1:8000/, then click on the register link and fill in the form to create a login. You should now be logged in with your newly created user. We’re going to use this user to authenticate for some of the message receiving later on.
5. The message model
Create the model
Create a Message
model along with the migration file by running the command:
php artisan make:model Message --migration
Add the $fillable
attribute into the Message class in your newly created Message.php
in app/Models
:
class Message extends Model
{
use HasFactory;
//insert the line below
protected $fillable = ['message'];
}
Add user_id
and message
columns to your messages schema by editing your newly created <creation_date_>create_messages_table.php
in database/migrations so it looks like this:
//database/migrations/<creation_date_>create_messages_table.php
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->timestamps();
//insert the lines below
$table->integer('user_id')->unsigned();
$table->text('message');
});
Now run the migrations script to add the messages table to our database:
php artisan migrate
User To Message Relationship
We will need to setup a one-to-many relationship between users and their many messages. In app/Models/User.php
add the messages()
function to the User class:
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = ['name','email','password',];
protected $hidden = ['password','remember_token',];
protected $casts = ['email_verified_at' => 'datetime',];
//Add the below function
public function messages()
{
return $this->hasMany(Message::class);
}
}
Then we define the inverse direction in the Message
class in app/Models/Message.php
:
class Message extends Model
{
use HasFactory;
protected $fillable = ['message'];
//Add the below function
public function user()
{
return $this->belongsTo(User::class);
}
}
We’re going to to use these functions to retrieve the messages for a user and vice versa.
7. Defining routes
Thats the database setup part done, we can move onto the server api. We will define some routes for our server. Routes are an important part of a Laravel application, so I recommend looking at their documentation here to get a feel for what is going on. Once you’ve done that, add these routes to routes/web.php
:
Route::get('/chat', [App\Http\Controllers\ChatsController::class, 'index']);
Route::get('/messages', [App\Http\Controllers\ChatsController::class, 'fetchMessages']);
Route::post('/messages', [App\Http\Controllers\ChatsController::class, 'sendMessage']);
Laravel has already put in a dashboard /home route for you. I’m going to leave this intact to keep this tutorial as simple as possible.
8. ChatsController
We’re going to create a ChatsController to handle the requests for the routes we just added. Take a brief look at the documentation here to understand this concept. Then run this command:
php artisan make:controller ChatsController
This will add a ChatsController.php file into app/Http/Controllers/. We’re going to add some functions to it, so first import our Message model at the top along with the existing use Illuminate\Http\Request;
:
use App\Models\Message;
We’re also going to add some authorisation in this controller, so import that as well:
use Illuminate\Support\Facades\Auth;
In the ChatsController class, add these functions:
class ChatsController extends Controller
{
//Add the below functions
public function __construct()
{
$this->middleware('auth');
}
public function index()
{
return view('chat');
}
public function fetchMessages()
{
return Message::with('user')->get();
}
public function sendMessage(Request $request)
{
$user = Auth::user();
$message = $user->messages()->create([
'message' => $request->input('message')
]);
return ['status' => 'Message Sent!'];
}
}
$this->middleware('auth')
in __contruct()
will only allow authorised users to access ChatsController’s methods. The index()
function will return a chat view which we will create later. fetchMessages()
returns a json of all messages with the user that sent that message. Lastly, the sendMessage()
function will use the create()
method to save the message into the database.
If you’re not familiar with database Object Relational Mappers and how our message gets saved into the messages table, read Laravel’s documentation to enhance your understanding.
We haven’t added the code to actually send the message yet. We will do that shortly.
9. Creating the Chat App view
We’re going to put the chat messages inside a Bootstrap 4 card to make it look nice. Create a new resources/views/chat.blade.php
file and paste this into it:
<!-- resources/views/chat.blade.php -->
@extends('layouts.app')
@section('content')
<div class="container">
<div class="card">
<div class="card-header">Chats</div>
<div class="card-body">
<chat-messages :messages="messages"></chat-messages>
</div>
<div class="card-footer">
<chat-form v-on:messagesent="addMessage" :user="{{ Auth::user() }}"></chat-form>
</div>
</div>
</div>
@endsection
Notice we have two custom tags: chat-messages
and chat-form
, these are Vue components which we’ll create soon. The chat-messages
component will display our chat messages and the chat-form
will provide an input field and a button to send the messages. Notice v-on:messagesent="addMessage" :user="{{ Auth::user() }}":
later on this will receive a messagesent event from the chat-form
component and pass the user property to the chat-form
.
Create a new ChatMessages.vue
file within resources/js/components
and paste the code below into it:
// resources/js/components/ChatMessages.vue
<template>
<ul class="chat">
<li class="left clearfix" v-for="message in messages" :key="message.id">
<div class="clearfix">
<div class="header">
<strong>
{{ message.user.name }}
</strong>
</div>
<p>
{{ message.message }}
</p>
</div>
</li>
</ul>
</template>
<script>
export default {
props: ["messages"],
};
</script>
This component accepts an array of messages as props
, loops through them and displays the name of the user who sent the message and the message body.
Next, create a new ChatForm.vue
file within resources/js/components
and paste the code below into it. I’ve added comments to explain what each part does.
//resources/js/components/ChatForm.vue
<template>
//Display an input field and a send button.
<div class="input-group">
//Input field.
<input
id="btn-input"
type="text"
name="message"
class="form-control input-sm"
placeholder="Type your message here..."
v-model="newMessage"
//Call sendMessage() when the enter key is pressed.
@keyup.enter="sendMessage"
/>
//Button
<span class="input-group-btn">
//Call sendMessage() this button is clicked.
<button class="btn btn-primary btn-sm" id="btn-chat" @click="sendMessage">
Send
</button>
</span>
</div>
</template>
<script>
export default {
//Takes the "user" props from <chat-form> … :user="{{ Auth::user() }}"></chat-form> in the parent chat.blade.php.
props: ["user"],
data() {
return {
newMessage: "",
};
},
methods: {
sendMessage() {
//Emit a "messagesent" event including the user who sent the message along with the message content
this.$emit("messagesent", {
user: this.user,
//newMessage is bound to the earlier "btn-input" input field
message: this.newMessage,
});
//Clear the input
this.newMessage = "";
},
},
};
</script>
If you take a look at resources/views/chat.blade.php
, you will notice in the <chat-form>
tag there is a v-on:messagesent="addMessage"
. This means that when a messagsent
event gets emitted from the above code, it will be handled by the addMessage()
method in the resources/js/app.js
root Vue instance. We have another tutorial on emitting in Vue here. That tutorial uses @
instead of v-on:
but they are equivalent.
Next, we need to register our component in the root Vue instance. Open resources/js/app.js
and add the following two components after the example-component Vue component that is already there:
//resources/js/app.js
Vue.component('example-component', require('./components/ExampleComponent.vue').default);
//Add these two components.
Vue.component('chat-messages', require('./components/ChatMessages.vue').default);
Vue.component('chat-form', require('./components/ChatForm.vue').default);
At the bottom of this file, there exists already a default root Vue instance app
. Add to it the following:
//app and el already exists.
const app = new Vue({
el: '#app',
//Store chat messages for display in this array.
data: {
messages: []
},
//Upon initialisation, run fetchMessages().
created() {
this.fetchMessages();
},
methods: {
fetchMessages() {
//GET request to the messages route in our Laravel server to fetch all the messages
axios.get('/messages').then(response => {
//Save the response in the messages array to display on the chat view
this.messages = response.data;
});
},
//Receives the message that was emitted from the ChatForm Vue component
addMessage(message) {
//Pushes it to the messages array
this.messages.push(message);
//POST request to the messages route with the message data in order for our Laravel server to broadcast it.
axios.post('/messages', message).then(response => {
console.log(response.data);
});
}
}
});
As you can see in the above Vue code, we are using the Axios javascript library to retrieve and send data to our Laravel backend server.
Finally, we should also create a link so the user can click to navigate to the chat view. In resources/views/home.blade.php, add this just under the {{ __('You are logged in!') }}
line:
<a href="{{ url('/chat') }}">Chat</a>
Before we go onto the next section, let’s take a look at what your application is capable of so far. Run the terminal commands npm run dev
and php artisan serve
, navigate to http://127.0.0.1:8000/ and login with the user you created back in section 4. Click on the “Chat” link you added just now on the home page to load the Vue chat app.
Now let’s try sending a message to another user.
Register a second user. Open another private window and log in is your second user. Send a message from your first user. Notice how the chat window doesn’t update on the second user’s chat app. However if you refresh the page, the message is now present. This is because the recipient chat window retrieved the message from the database instead of receiving live updates from Pusher Channels. We will do this next.
10. Broadcasting Message Sent Event
To add the realtime interactions to our chat app, we need to broadcast some kind of events based on some activities. In our case, we’ll fire a MessageSent
when a user sends a message. First, we need to create an event called MessageSent
:
php artisan make:event MessageSent
This will create a new MessageSent
event class within the app/Events
directory. Open it and import our user and message models:
//app/Events/MessageSent.php
namespace App\Events;
//Add the following two models
use App\Models\User;
use App\Models\Message;
...
...
Add the implements ShouldBroadcast
interface to the MessageSent
class:
class MessageSent implements ShouldBroadcast
{
...
}
Inside the class, add these two public properties:
public $user;
public $message;
There is already an empty __construct()
function, modify it to the following:
public function __construct(User $user, Message $message)
{
$this->user = $user;
$this->message = $message;
}
Set the channel to broadcast onto the chat private channel:
public function broadcastOn()
{
return new PrivateChannel('chat');
}
Since we created a private channel by returning a PrivateChannel('chat')
instance, only authenticated users will be able to listen to it. So, we need a way to authorise that the currently authenticated user can actually listen on the channel. This can be done by adding the following channel in the routes/channels.php
file:
Broadcast::channel('chat', function ($user) {
return Auth::check();
});
Finally, we need to go back to ChatsController
and add in the statements to actually send a message onto Channels (we didn’t before). In app/Http/Controllers/ChatsController.php
import the MessageSent
event we just made:
use App\Events\MessageSent;
Just before the return statement at the bottom of the sendMessage()
function, add the important broadcast()
call to broadcast to Pusher.
broadcast(new MessageSent($user, $message))->toOthers();
We’ve now finished the Laravel server side part! Run php artisan serve
and load the chat app. Open your debug console in your Pusher dashboard and send a message from the chat app. You will see this log:
If Pusher isn’t getting the request from you, you may able to find an error message in storage/logs/laravel.log. Failing that, compare your code with my repository.
For our next and final part, we’re going to get Laravel Echo to listen for new messages.
11. Laravel Echo
We will use Laravel Echo to subscribe to the private chat channel and listen()
for the MessageSent
event. Add this in resources/js/app.js
inside created(){…}
after this.fetchMessages()
:
window.Echo.private('chat')
.listen('MessageSent', (e) => {
this.messages.push({
message: e.message.message,
user: e.user
});
});
Upon receiving a MessageSent
event, the above code will push the data onto the messages
array, which will update the message chat history on the web page.
Compile it for the last time and then run it:
npm run dev
php artisan serve
Open two private web browser windows, log in as two users, and send a message from one of them. You will see the other window immediately update. What happened was the chat app which had sent the message had requested Pusher Channels to broadcast the message to other clients, which in this case was the chat app in the other window.
If the other chat app isn’t immediately updating, use the web browser Javascript console to see if there are any errors. If you’re getting 404 errors (ignore the sourcemap warnings), use the php artisan route:list
command to display all routes which should help you spot any missing ones.
If you want to add a typing indicator, take a look at another one of our tutorials here.
Conclusion
You saw how Laravel already has some built in integration for Pusher Channels. With a little bit of extra configuration, you’re good to start sending out messages from Laravel, and receiving them in realtime with Echo, using Pusher in between.
13 October 2021
by Benjamin Tang