Idea behind this blog post is creation of real-time chat application using WebSockets, PHP and Vue which will allow users to chat anonymously.
WebSocket is a computer communication protocol, providing full-duplex communication channels. Basically it allows bi-directional communication between user and server without reloading the page or client polling the server for the changes that needs to be fetched.
Requirements:
- Laravel 5.5
- Redis-server 3.0.6
- MySQL 14.14
- node.js 8.9.4
I suppose that it will work with next releases of above mentioned software, but for the sake of stability use exact versions.
I will also assume that you have basic familiarity with Laravel and Vue frameworks and the convenient tooling they provide.
Now let's start. We will generate new Laravel project and name it simple-chat-app
. Then we will create database inside of our favorite database-client i.e. (Sequelpro, DBeaver or even the good old mysql-cli) and up creation we will enter into root of application and search for .env
file. If there is no .env
, rename env.example
to .env
and open it with your favorite text editor or IDE. Enter the name of recently created database to DB_DATABASE
, username to DB_USERNAME
, password to DB_PASSWORD
. And while you are here change BROADCAST_DRIVER
to redis
.
So next step will be creation of migration and model for the message records that will contain data we'll be broadcasting to the users. We can do that by running artisan command:
php artisan make:model -m Message
Let's modify generated migration file.
// ...
public function up()
{
Schema::create('messages', function (Blueprint $table) {
$table->increments('id');
$table->text('content');
$table->timestamps();
});
}
// ...
Now we should migrate database with following command:
php artisan migrate
Then let's go back to PHP and create two routes - one for fetching all the messages from database and another for posting them. To accomplish that task we will edit web.php
file.
use App\Message;
// Return index page with the Vue component we will crete soon
Route::get('/', function () {
return view('welcome');
});
// Return all messages that will populate our chat messages
Route::get('/getAll', function () {
$messages = Message::take(200)->pluck('content');
return $messages;
});
// Allows us to post new message
Route::post('/post', function () {
$message = new Message();
$content = request('message');
$message->content = $content;
$message->save();
return $content;
});
Let's modify default welcome.blade.php
template to include Vue component that will act as a chat box. We'll remove all but bare minimum required for our application to run and we will add <chat-box>
component tag.
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Anonymous Chat App</title>
{{-- Script hosted on running laravel-echo-server --}}
<script src="//{{ Request::getHost() }}:6001/socket.io/socket.io.js"></script>
</head>
<body>
<div class="content">
<div id="app">
<chat-box></chat-box>
</div>
</div>
<script src="{{ mix('js/app.js') }}"></script>
</body>
</html>
Now we will have to create chat component. We can do it by renaming ExampleComponent.vue
located inside of the components
directory.
<template>
<div>
<p v-for="message in messages">{{ message }}</p>
<input v-model="text">
<button @click="postMessage" :disabled="!contentExists">submit</button>
</div>
</template>
<script>
export default {
data() {
return {
text: '',
messages: []
}
},
computed: {
contentExists() {
return this.text.length > 0;
}
},
methods: {
postMessage() {
axios.post('/post', {message: this.text}).then(({data}) => {
this.messages.push(data);
this.text = '';
});
}
},
created() {
axios.get('/getAll').then(({data}) => {
this.messages = data;
});
}
}
</script>
As you can see chat box consists of paragraphs that are created from messages
array we fetched on create
life hook of the component. postMessage
method straight forwardly sends data from text
variable which is two-way binded with input filed inside of template tags and on success it clears the value of the text
and populate messages
array.
At the end we will have to register it inside of app.js
file
// ...
Vue.component('chat-box', require('./components/ChatBox.vue'));
// ...
For the fronend we will install default npm packages Laravel provided us when we initialized the project. We will use them to build Vue component. Inside the shell run:
npm install
We will use Redis as message broadcaster. So we'll install, as official documentation says, predis
package. Inside of your terminal:
composer require predis/predis
To enable broadcasting from our backend app we will have to uncomment the line containing BroadcastServiceProvider::class
from app.php
.
// ...
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// Line below should be uncomment
App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
// ...
Next we will create an event that will broadcast message to the clients connected with the socket.
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class MessageSent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public $message;
public function __construct($message)
{
$this->message = $message;
$this->dontBroadcastToCurrentUser();
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new Channel('public');
}
}
We made few changes on our generated Event by implementing interface ShouldBroadcast
. Then added public property $message
which will hold our message. Also we changed PrivateChannel
to Channel
inside of broadcastOn
method . Private and Presence channels are beyond the scope of this blog post.
Laravel-Echo
is a npm package that makes it painless to subscribe to channels and listen for events. We should install it by running following command:
npm install laravel-echo
Next we will need to install socket.io server. There is great community package tlaverdure/laravel-echo-server
. We will install it globally using command
npm install -g laravel-echo-server
Now, as documentations says, let's run following command inside of the root of our application:
laravel-echo-server init
You will be prompted by the init script, answer as following:
? Do you want to run this server in development mode? Yes
? Which port would you like to serve from? 6001
? Which database would you like to use to store presence channel members? redis
? Enter the host of your Laravel authentication server. http://localhost
? Will you be serving on http or https? http
? Do you want to generate a client ID/Key for HTTP API? Yes
? Do you want to setup cross domain access to the API? No
Prompt will generate laravel-echo-server.json
file that we can change later to suit our needs.
Following we should run the server from project root directory:
laravel-echo-server start
We will have to make few changes in bootstrap.js
.
// ...
import Echo from 'laravel-echo'
// window.Pusher = require('pusher-js');
window.Echo = new Echo({
broadcaster: 'socket.io',
host: window.location.hostname + ':6001'
});
We just imported and binded Echo
object to the window. Next step is to refactor ChatBox.vue
to act on received event.
// ...
created() {
axios.get('/getAll').then(({data}) => {
this.messages = data;
});
// Registered client on public channel to listen to MessageSent event
Echo.channel('public').listen('MessageSent', ({message}) => {
this.messages.push(message);
});
}
// ...
Finally we will have to broadcast event from our backed server we will do that by tweaking web.php
route.
// ...
Route::post('/post', function () {
$message = new Message();
$content = request('message');
$message->content = $content;
$message->save();
event(new MessageSent($content));
return $content;
// ...
We included MessageSent
event, and we told the router to emit it when user post message.
Also, don't forget to build npm modules and Vue component, from you terminal emulator run:
npm run dev
Now you can chat on public channel with your peers, yeey :).
Related documentation: