This tutorial will help you build a realtime commenting system with Laravel, Vue.js and Pusher. We’ll create a basic landing page, a comments feed, and a submission form where users can submit comments that will be added to the page and viewable instantly. This tutorial is the Laravel version of this one, and when setting up Vuex and Vue.js you can refer to the same sections in that tutorial.
Now on to building our app!
Your final app should be looking like this:
In order to follow this tutorial a basic to good understanding of Vue.js and Laravel is required, as we’ll be using these frameworks throughout this tutorial. Also ensure you have Node.js installed on your machine or Yarn.
We’ll be using these tools to build our application so make sure to have them installed on your machine:
To get started with Pusher Channels, sign up for a free Pusher account. Then go to the dashboard and create a new Channels app.
Then retrieve your credentials from the API Keys tab, and make note of it as we’ll use them later in the tutorial.
To get started we’ll install a new Laravel application using the Laravel CLI. We’ll run the following command:
laravel new live_comments
Once the installation is finished run the following command to move to your app directory:
cd live_comments
Now we’ll install our node dependencies, first paste this in your package.json
file:
1//live_comments/package.json 2 { 3 "private": true, 4 "scripts": { 5 "dev": "npm run development", 6 "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 7 "watch": "npm run development -- --watch", 8 "watch-poll": "npm run watch -- --watch-poll", 9 "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 10 "prod": "npm run production", 11 "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 12 }, 13 "devDependencies": { 14 "axios": "^0.18", 15 "bootstrap": "^4.0.0", 16 "cross-env": "^5.1", 17 "jquery": "^3.2", 18 "laravel-mix": "^2.0", 19 "lodash": "^4.17.5", 20 "popper.js": "^1.12", 21 "vue": "^2.5.7", 22 "vuex": "^3.0.1", 23 "moment": "^2.22.2", 24 "pusher-js": "^4.2.2" 25 } 26 }
Then run npm install
or yarn
to install the dependencies. It’s up to you.
After this step, add the following to your .env
file in the root of your project directory. Ensure to replace the placeholders with your keys from Pusher.
1PUSHER_APP_ID=YOUR_PUSHER_APP_ID 2 PUSHER_APP_KEY=YOUR_PUSHER_APP_KEY 3 PUSHER_APP_SECRET=YOUR_PUSHER_APP_SECRET 4 PUSHER_APP_CLUSTER=YOUR_PUSHER_APP_CLUSTER
We’ll use SQLite as our database. Create a database.sqlite file in the database directory, and amend the .env file like this:
1DB_CONNECTION=sqlite 2 DB_DATABASE=/absolute/path/to/database.sqlite
Refer to this section on Laravel website for more relevant information.
Now, let’s build our database structure. We’ll use again Laravel CLI for that.
Run this command:
php artisan make:model Comment -mc
The above command will generate the Comment model as well as its migration and its controller CommentController.php
for us.
Open your Comment.php
file and paste this:
1//live_comments/app/Comment.php 2 3 <?php 4 5 namespace App; 6 7 use Illuminate\Database\Eloquent\Model; 8 9 class Comment extends Model { 10 11 // 12 protected $fillable = ['content', 'author']; 13 }
Next copy and paste this piece of code in your comment migration file:
1//live_comments/database/migrations/*_create_comments_table.php 2 <?php 3 4 use Illuminate\Database\Migrations\Migration; 5 use Illuminate\Database\Schema\Blueprint; 6 use Illuminate\Support\Facades\Schema; 7 8 class CreateCommentsTable extends Migration 9 { 10 /** 11 * Run the migrations. 12 * 13 * @return void 14 */ 15 16 public function up() 17 { 18 Schema::create('comments', function (Blueprint $table) { 19 $table->increments('id'); 20 $table->string('content'); 21 $table->string('author'); 22 $table->timestamps(); 23 }); 24 } 25 26 /** 27 * Reverse the migrations. 28 * 29 * @return void 30 */ 31 32 public function down() 33 { 34 Schema::dropIfExists('comments'); 35 } 36 }
Then run php artisan migrate
to run the migration.
In this section we’ll define our app endpoints and define the logic behind our CommentController.php
.
We’ll create three basic routes for our application, one for rendering our app view, one for fetching comments from the database and the last one for storing comments into the database.
Paste the following into api.php
:
1//live_comments/routes/api.php 2 <?php 3 use Illuminate\Support\Facades\Route; 4 5 Route::get('/', 'CommentController@index'); 6 7 Route::prefix('api')->group(function () { 8 Route::get('/comments', 'CommentController@fetchComments'); 9 Route::post('/comments', 'CommentController@store'); 10 });
And amend web.php
like the following
1//live_comments/routes/web.php 2 <?php 3 use Illuminate\Support\Facades\Route; 4 Route::get('/', 'CommentController@index');
Now let’s define our controller logic. Our controller functions will be responsible for actions to handle when some requests reach our API endpoints.
Open your CommentController
file and paste the following code:
1//live_comments/app/Http/Controllers/CommentController.php 2 <?php 3 4 namespace App\Http\Controllers; 5 6 use App\Comment; 7 use App\Events\CommentEvent; 8 use Illuminate\Http\Request; 9 10 class CommentController extends Controller 11 { 12 // 13 14 public function index() 15 { 16 17 return view('comments'); 18 } 19 20 public function fetchComments() 21 { 22 $comments = Comment::all(); 23 24 return response()->json($comments); 25 } 26 27 public function store(Request $request) 28 { 29 $comment = Comment::create($request->all()); 30 31 event(new CommentEvent($comment)); 32 return response()->json('ok'); 33 34 } 35 }
You can notice three functions in the code above:
index
renders the comment.edge
file(that we’ll create later in this tutorial) in the resources/views
directory (which is where views are stored in Adonis).fetchComments
fetches comments from our database and returns them in a JSON formatstore
creates a new Comment
instance with the request queries and returns a response.Well you may have noticed this line: event(new CommentEvent($comment))
. It broadcasts an event with the new comment to the client-side of our app using Laravel broadcasting. We’ll see how to create this event in the next part of the tutorial.
Our SearchEvent
event will be emitted whenever a comment is submit by a user. Enough talk, let’s focus on the code. Let’s create our CommentEvent
by running the following command in your terminal: php artisan make:event CommentEvent
.
Now open your CommentEvent
file and paste the following:
1//live_comments/app/Events/CommentEvent.php 2 <?php 3 4 namespace App\Events; 5 6 use Illuminate\Broadcasting\Channel; 7 use Illuminate\Broadcasting\InteractsWithSockets; 8 use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; 9 use Illuminate\Foundation\Events\Dispatchable; 10 use Illuminate\Queue\SerializesModels; 11 12 class CommentEvent implements ShouldBroadcastNow 13 { 14 use Dispatchable, InteractsWithSockets, SerializesModels; 15 16 public $comment; 17 18 /** 19 * Create a new event instance. 20 * 21 * @param $comment 22 * 23 * @return void 24 */ 25 26 public function __construct($comment) 27 { 28 // 29 $this->comment = $comment; 30 } 31 32 /** 33 * Get the channels the event should broadcast on. 34 * 35 * @return \Illuminate\Broadcasting\Channel|array 36 */ 37 38 public function broadcastOn() 39 { 40 return new Channel('comment-channel'); 41 } 42 43 public function broadcastAs() 44 { 45 return 'newComment'; 46 } 47 48 49 }
Our class constructor initializes a comment that is nothing but the new submit comment. We have two additional functions that may seem strange to you:
broadcastAs
: customizes the broadcast name because by default Laravel uses the event’s class name.broadcastOn
: defines the channel comment-channel
(which we’ll set up further on the tutorial) on which our event should be broadcast.According to Laravel documentation about event broadcasting, before broadcasting any events, you will first need to register the App\Providers\BroadcastServiceProvider
. In fresh Laravel applications, you only need to uncomment this provider in the providers
array of your ../config/app.php
configuration file. This provider will allow you to register the broadcast authorization routes and callbacks.
If this is done, you have to tell Laravel to use Pusher Channels to broadcast events. Open your .env
file and ensure you have this line: BROADCAST_DRIVER=pusher
As we are broadcasting our events over Pusher, we should install the Pusher Channels PHP SDK using the Composer package manager:
composer require pusher/pusher-php-server "~3.0"
Laravel broadcasts events on well defined channels. As said above our event should be broadcast on comment-channel
channel. It’s time to set it up. Paste the following code in your channels.php
file:
1//live_comments/routes/channels.php 2 Broadcast::channel('comment-channel', function () { 3 return true; 4 });
As we aren’t using Laravel auth, we return true
in the function callback so that everybody can use this channel to broadcast events.
Now amend your bootstrap.js
file like the following:
1//live_comments/resources/js/bootstrap.js 2 3 window._ = require('lodash'); 4 5 window.axios = require('axios'); 6 window.moment = require('moment') 7 8 // import 'vue-tel-input/dist/vue-tel-input.css'; 9 10 window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 11 window.axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; 12 window.axios.defaults.headers.common.crossDomain = true; 13 window.axios.defaults.baseURL = '/api'; 14 15 let token = document.head.querySelector('meta[name="csrf-token"]'); 16 17 if (token) { 18 window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; 19 } else { 20 console.error('CSRF token not found: https://adonisjs.com/docs/4.1/csrf'); 21 } 22 23 24 window.Pusher = require('pusher-js');
We make our pusher-js
package global in order to use with no hassle everywhere and to listen to events on client-side.
Our app is ready to broadcast and consume events in realtime using Pusher Channels. Let’s focus now on the frontend of your app.
We’ll be using the Vuex library to centralize our data and control the way it is mutated throughout our application.
Vuex state is a single object that contains all our application data. So let’s create ../resources/js/store/state.js
and paste this code inside:
1let state = { 2 comments: [] 3 } 4 export default state
The comments
key is an array responsible to store our database comments.
With help of getters we can compute derived based on our data store state. Create ../resources/js/store/getters.js
and paste this code inside
1let getters = { 2 comments: state => { 3 return state.comments 4 } 5 } 6 7 export default getters
Mutations allow us to perform some changes on our data. Create ../resources/js/store/mutations.js
and paste this piece of code inside:
1let mutations = { 2 GET_COMMENTS(state, comments) { 3 state.comments = comments 4 }, 5 ADD_COMMENT(state, comment) { 6 state.comments = [...state.comments, comment] 7 } 8 } 9 10 export default mutations
Our mutations
object has two functions:
GET_COMMENTS
is responsible for getting our comments data from a database or webserver.ADD_COMMENT
is responsible for adding a new comment to our comments array using the ES6 spread operator.Vuex actions allow us to perform asynchronous operations over our data. Create the file ../resources/js/store/actions.js
and paste the following code:
1let actions = { 2 ADD_COMMENT({commit}, comment) { 3 4 return new Promise((resolve, reject) => { 5 axios.post(`/comments`, comment) 6 .then(response => { 7 resolve(response) 8 }).catch(err => { 9 reject(err) 10 }) 11 }) 12 }, 13 14 GET_COMMENTS({commit}) { 15 axios.get('/comments') 16 .then(res => { 17 { 18 commit('GET_COMMENTS', res.data) 19 } 20 }) 21 .catch(err => { 22 console.log(err) 23 }) 24 } 25 } 26 27 export default actions
We have defined two actions and each of them is responsible for a single operation, either comments post or comments fetch. They both perform asynchronous calls to our API routes.
ADD_COMMENT
sends a post request to our /api/comments
with the new comment to create and returns a new promise (later in this tutorial we’ll handle the returned promise). This action is dispatched whenever a user submits a comment.
GET_COMMENTS
makes a get request to our api/comments
endpoint to get our database comments and commits the request result with GET_COMMENTS
mutation.
Create the file ../resources/js/store/index.js
and paste this code inside:
1import Vue from 'vue' 2 import Vuex from 'vuex' 3 import actions from './actions' 4 import mutations from './mutations' 5 import getters from './getters' 6 import state from "./state"; 7 8 Vue.use(Vuex); 9 10 export default new Vuex.Store({ 11 state, 12 mutations, 13 getters, 14 actions 15 })
Next, we will export our store and add it to the Vue instance.
Add this code to your ../resouces/js/app.js
file.
1require('./bootstrap') 2 3 window.Vue = require('vue'); 4 5 import store from './store/index' 6 7 Vue.component('comment', require('./components/Comment')); 8 Vue.component('comments', require('./components/Comments')) 9 Vue.component('new-comment', require('./components/NewComment')) 10 11 const app = new Vue({ 12 el: '#app', 13 store 14 });
The code above globally registers three Vue components, Comment.vue
,Comments.vue
and NewComment.vue
that we’ll build in the next part of this tutorial.
We’ll build three Vue components for our app, the Comment.vue
component, the Comments.vue
and the NewComment.vue
component, each of them responsible for a single functionality.
The Comment.vue
component is responsible for encapsulating details about a single comment instance from the database and rendering it in a proper and styled way.
Paste the following inside your Comment.vue
component.
1//../resources/js/components/Comment.vue 2 3 <template> 4 <li class="comment-wrapper animate slideInLeft "> 5 <div class="profile"> 6 <img :src="avatar" alt=""></div> 7 <div class="msg has-shadow"> 8 <div class="msg-body"><p class="name">{{comment.author}} <span class="date">{{posted_at}}</span></p> 9 <p class="content">{{comment.content}}</p></div> 10 </div> 11 </li> 12 </template> 13 14 <script> 15 export default { 16 name: "Comment", 17 props: ['comment'], 18 computed: { 19 posted_at() { 20 return moment(this.comment.created_at).format('MMMM Do YYYY') 21 }, 22 avatar() { 23 return `https://api.adorable.io/avatars/48/${this.comment.author}@adorable.io.png` 24 } 25 } 26 } 27 </script> 28 29 <style lang="scss" scoped> 30 .comment-wrapper { 31 list-style: none; 32 text-align: left; 33 overflow: hidden; 34 margin-bottom: 2em; 35 padding: .4em; 36 37 .profile { 38 width: 80px; 39 float: left; 40 } 41 42 .msg-body { 43 padding: .8em; 44 color: #666; 45 line-height: 1.5; 46 } 47 48 .msg { 49 width: 86%; 50 float: left; 51 background-color: #fff; 52 border-radius: 0 5px 5px 5px; 53 position: relative; 54 &::after { 55 content: " "; 56 position: absolute; 57 left: -13px; 58 top: 0; 59 border: 14px solid transparent; 60 border-top-color: #fff; 61 } 62 } 63 64 .date { 65 float: right; 66 } 67 .name { 68 margin: 0; 69 color: #999; 70 font-weight: 700; 71 font-size: .8em; 72 } 73 74 p:last-child { 75 margin-top: .6em; 76 margin-bottom: 0; 77 } 78 79 } 80 81 82 </style>
Our Comment.vue
component takes a comment
property whose details we simply render in the component body. We also defined two computed
properties, posted_at
to parse the Moment.js library with the comment
posted date, and avatar
to generate an avatar for the comment author using this API.
In the style
block we’ve defined some styles to our comment component in order to make things look more beautiful.
This component will render comment items from the database.
Create your Comments.vue
component and paste this code inside:
1../resources/js/components/Comments.vue 2 3 <template> 4 <div class="container"> 5 <ul class="comment-list"> 6 <Comment :key="comment.id" v-for="comment in comments" :comment="comment"></Comment> 7 </ul> 8 </div> 9 </template> 10 11 <script> 12 import {mapGetters} from 'vuex' 13 import Comment from './Comment' 14 15 export default { 16 name: "Comments", 17 components: {Comment}, 18 mounted() { 19 this.$store.dispatch('GET_COMMENTS') 20 21 //use your own credentials you get from Pusher 22 let pusher = new Pusher(`YOUR_PUSHER_APP_ID`, { 23 cluster: `YOUR_PUSHER_CLUSTER`, 24 encrypted: false 25 }); 26 27 //Subscribe to the channel we specified in our Adonis Application 28 let channel = pusher.subscribe('comment-channel') 29 30 channel.bind('new-comment', (data) => { 31 this.$store.commit('ADD_COMMENT', data.comment) 32 }) 33 }, 34 computed: { 35 ...mapGetters([ 36 'comments' 37 ]) 38 } 39 } 40 </script> 41 42 <style scoped> 43 .comment-list { 44 padding: 1em 0; 45 margin-bottom: 15px; 46 } 47 48 </style>
NOTE: First don’t forget to add your Pusher credentials in your Vue template.
In the template
section of this code, we loop through our comments array and render for each loop iteration a Comment.vue
component imported with the current comment iterated as a property.
In the mounted
hook function we dispatched the GET_COMMENTS
action. The action defined above sends a get request to our database to fetch posted comments. Then, we initialized a Pusher instance using the credentials obtained earlier when creating our Pusher app. Next, we subscribed to the comment-channel
and listened to the new-comment
event in order to commit the ADD_COMMENT
mutation with the new comment pulled in by the event.
We also used the Vuex helper function …mapGetters()
to access our comments state as computed
property. In this component we also defined some styles to beautify our interface in the style
block.
Our third component is responsible for displaying a form to our users for comment posting. It should also send a request to our database when a user submits his comment. Let’s create the NewComment.vue
component, copy and paste this code inside:
1../resources/js/components/NewComment.vue 2 <template> 3 <div id="commentForm" class="box has-shadow has-background-white"> 4 5 <form @keyup.enter="postComment"> 6 <div class="field has-margin-top"> 7 8 <div class="field has-margin-top"> 9 <label class="label">Your name</label> 10 <div class="control"> 11 <input type="text" placeholder="Your name" class="input is-medium" v-model="comment.author"> 12 </div> 13 14 </div> 15 <div class="field has-margin-top"> 16 <label class="label">Your comment</label> 17 <div class="control"> 18 <textarea 19 style="height:100px;" 20 name="comment" 21 class="input is-medium" autocomplete="true" v-model="comment.content" 22 placeholder="lorem ipsum"></textarea> 23 </div> 24 25 </div> 26 <div class="control has-margin-top"> 27 <button style="background-color: #47b784" :class="{'is-loading': submit}" 28 class="button has-shadow is-medium has-text-white" 29 :disabled="!isValid" 30 @click.prevent="postComment" 31 type="submit"> Submit 32 </button> 33 </div> 34 </div> 35 </form> 36 <br> 37 </div> 38 </template> 39 40 <script> 41 export default { 42 name: "NewComment", 43 data() { 44 return { 45 submit: false, 46 comment: { 47 content: '', 48 author: '', 49 } 50 } 51 }, 52 methods: { 53 postComment() { 54 this.submit = true; 55 this.$store.dispatch('ADD_COMMENT', this.comment) 56 .then(response => { 57 this.submit = false; 58 if (response.data === 'ok') 59 console.log('success') 60 }).catch(err => { 61 this.submit = false 62 }) 63 64 }, 65 }, 66 computed: { 67 isValid() { 68 return this.comment.content !== '' && this.comment.author !== '' 69 } 70 } 71 } 72 </script> 73 74 <style scoped> 75 .has-margin-top { 76 margin-top: 15px; 77 } 78 79 </style>
We bind our comment
data to our comment content and author name fields using the Vue.js v-model
directive. We handled the form submission with the postComment
function inside which we dispatch the ADD_COMMENT
mutation with the comment data entered by the user. We also defined isValid
as a computed property that we use to disable the submit button if the two required fields are empty.
Now, let’s create our comments.blade.php
file which contains our Vue.js components. Then paste this code inside:
1//live_comments/resources/views/comments.blade.php 2 3 <!DOCTYPE html> 4 <html lang="en"> 5 <head> 6 <meta charset="UTF-8"/> 7 <title>Live commenting system with Laravel and Pusher</title> 8 <meta name="csrf-token" content="{{ csrf_token() }}"> 9 <meta name="viewport" 10 content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> 11 12 <!-- Bootstrap core CSS --> 13 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css"/> 14 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css"/> 15 16 <style> 17 html { 18 background: radial-gradient(ellipse at center, #fff 0, #ededfd 100%); 19 } 20 21 #app { 22 width: 60%; 23 margin: 4rem auto; 24 } 25 26 .container { 27 margin: 0 auto; 28 position: relative; 29 width: unset; 30 } 31 32 .question-wrapper { 33 text-align: center; 34 } 35 36 .has-shadow { 37 box-shadow: 0 4px 8px -2px rgba(0, 0, 0, 0.05) !important; 38 } 39 40 </style> 41 </head> 42 <body> 43 44 45 <div id="app"> 46 47 <div class="container"> 48 <div class="question-wrapper"> 49 <img width="200" src="{{ asset('images/adonuxt.png') }}" alt=""> 50 <h5 class="is-size-2" style="color: #220052;"> 51 What do you think about <span style="color: #47b784;">Laravel</span>?</h5> 52 <br> 53 <a href="#commentForm" class="button is-medium has-shadow has-text-white" style="background-col 54 or: #47b784">Comment</a> 55 </div> 56 57 <br><br> 58 <comments></comments> 59 <new-comment></new-comment> 60 </div> 61 </div> 62 63 <script async src="{{mix('js/app.js')}}"></script> 64 65 </body> 66 67 </html>
We are almost done! Now open your terminal and run npm run dev
to build your app. This can take a few seconds. After this step, run php artisan serve
and open your browser to localhost:8000
to see your app working fine. Try posting a new comment! You should see your comment added in realtime 😎.
Note: If you encounter a 500 error when trying to submit a comment, it’s sure that you have to disable Pusher encryption. Open these files
../config/broadcasting.php
and../resources/js/bootstrap.js
and make sure you disable Pusher encryptionencrypted: false
in both of them.
In this tutorial, we have covered how to create a live commenting system using Laravel, Vue.js and Pusher Channels. You can get the full source code here.