In the previous part of this series, we finished building the backend of the application using Vue. We were able to add the create and update component, which is used for creating a new post and updating an existing post.
Here’s a screen recording of what we have been able to achieve:
In this final part of the series, we will be adding support for comments. We will also ensure that the comments on each post are updated in realtime, so a user doesn’t have to refresh the page to see new comments.
When we are done, our application will have new features and will work like this:
To follow along with this series, a few things are required:
When we were creating the API, we did not add the support for comments to the post resource, so we will have to do so now. Open the API project in your text editor as we will be modifying the project a little.
The first thing we want to do is create a model, controller, and a migration for the comment resource. To do this, open your terminal and cd
to the project directory and run the following command:
$ php artisan make:model Comment -mc
The command above will create a model called Comment
, a controller called CommentController
, and a migration file in the database/migrations
directory.
To update the comments migration navigate to the database/migrations
folder and find the newly created migration file for the Comment
model. Let’s update the up()
method in the file:
1// File: ./database/migrations/*_create_comments_table.php 2 public function up() 3 { 4 Schema::create('comments', function (Blueprint $table) { 5 $table->increments('id'); 6 $table->timestamps(); 7 $table->integer('user_id')->unsigned(); 8 $table->integer('post_id')->unsigned(); 9 $table->text('body'); 10 }); 11 }
We included user_id
and post_id
fields because we intend to create a link between the comments, users, and posts. The body
field will contain the actual comment.
In this application, a comment will belong to a user and a post because a user can make a comment on a specific post, so we need to define the relationship that ties everything up.
Open the User
model and include this method:
1// File: ./app/User.php 2 public function comments() 3 { 4 return $this->hasMany(Comment::class); 5 }
This is a relationship that simply says that a user can have many comments. Now let’s define the same relationship on the Post
model. Open the Post.php
file and include this method:
1// File: ./app/Post.php 2 public function comments() 3 { 4 return $this->hasMany(Comment::class); 5 }
Finally, we will include two methods in the Comment
model to complete the second half of the relationships we defined in the User
and Post
models.
Open the app/Comment.php
file and include these methods:
1// File: ./app/Comment.php 2 public function user() 3 { 4 return $this->belongsTo(User::class); 5 } 6 7 public function post() 8 { 9 return $this->belongsTo(Post::class); 10 }
Since we want to be able to mass assign data to specific fields of a comment instance during comment creation, we will include this array of permitted assignments in the app/Comment.php
file:
protected $fillable = ['user_id', 'post_id', 'body'];
We can now run our database migration for our comments:
$ php artisan migrate
We already said that the comments will have a realtime functionality and we will be building this using Pusher Channels, so we need to enable Laravel’s event broadcasting feature.
Open the config/app.php
file and uncomment the following line in the providers
array:
App\Providers\BroadcastServiceProvider
Next, we need to configure the broadcast driver in the .env
file:
BROADCAST_DRIVER=pusher
Let’s pull in the Pusher PHP SDK using composer:
$ composer require pusher/pusher-php-server
To get started with Pusher Channels, sign up for a free Pusher account. Then go to the dashboard and create a new Channels app.
Once you have created an app, we will use the app details to configure pusher in the .env
file:
1PUSHER_APP_ID=xxxxxx 2 PUSHER_APP_KEY=xxxxxxxxxxxxxxxxxxxx 3 PUSHER_APP_SECRET=xxxxxxxxxxxxxxxxxxxx 4 PUSHER_APP_CLUSTER=xx
Update the Pusher Channels keys with the app credentials provided for you under the Keys section on the Overview tab on the Pusher dashboard.
To make the comment update realtime, we have to broadcast an event based on the comment creation activity. We will create a new event and call it CommentSent
. It is to be fired when there is a successful creation of a new comment.
Run command in your terminal:
php artisan make:event CommentSent
There will be a newly created file in the app\Events
directory, open the CommentSent.php
file and ensure that it implements the ShouldBroadcast
interface.
Open and replace the file with the following code:
1// File: ./app/Events/CommentSent.php 2 <?php 3 4 namespace App\Events; 5 6 use App\Comment; 7 use App\User; 8 use Illuminate\Broadcasting\Channel; 9 use Illuminate\Queue\SerializesModels; 10 use Illuminate\Broadcasting\PrivateChannel; 11 use Illuminate\Broadcasting\PresenceChannel; 12 use Illuminate\Foundation\Events\Dispatchable; 13 use Illuminate\Broadcasting\InteractsWithSockets; 14 use Illuminate\Contracts\Broadcasting\ShouldBroadcast; 15 16 class CommentSent implements ShouldBroadcast 17 { 18 use Dispatchable, InteractsWithSockets, SerializesModels; 19 20 public $user; 21 22 public $comment; 23 24 public function __construct(User $user, Comment $comment) 25 { 26 $this->user = $user; 27 28 $this->comment = $comment; 29 } 30 31 public function broadcastOn() 32 { 33 return new PrivateChannel('comment'); 34 } 35 }
In the code above, we created two public properties, user
and comment
, to hold the data that will be passed to the channel we are broadcasting on. We also created a private channel called comment
. We are using a private channel so that only authenticated clients can subscribe to the channel.
We created a controller for the comment model earlier but we haven’t defined the web routes that will redirect requests to be handled by that controller.
Open the routes/web.php
file and include the code below:
1// File: ./routes/web.php 2 Route::get('/{post}/comments', 'CommentController@index'); 3 Route::post('/{post}/comments', 'CommentController@store');
We need to include two methods in the CommentController.php
file, these methods will be responsible for storing and retrieving methods. In the store()
method, we will also be broadcasting an event when a new comment is created.
Open the CommentController.php
file and replace its contents with the code below:
1// File: ./app/Http/Controllers/CommentController.php 2 <?php 3 4 namespace App\Http\Controllers; 5 6 use App\Comment; 7 use App\Events\CommentSent; 8 use App\Post; 9 use Illuminate\Http\Request; 10 11 class CommentController extends Controller 12 { 13 public function store(Post $post) 14 { 15 $this->validate(request(), [ 16 'body' => 'required', 17 ]); 18 19 $user = auth()->user(); 20 21 $comment = Comment::create([ 22 'user_id' => $user->id, 23 'post_id' => $post->id, 24 'body' => request('body'), 25 ]); 26 27 broadcast(new CommentSent($user, $comment))->toOthers(); 28 29 return ['status' => 'Message Sent!']; 30 } 31 32 public function index(Post $post) 33 { 34 return $post->comments()->with('user')->get(); 35 } 36 }
In the store
method above, we are validating then creating a new post comment. After the comment has been created, we broadcast the CommentSent
event to other clients so they can update their comments list in realtime.
In the index
method we just return the comments belonging to a post along with the user that made the comment.
Let’s add a layer of authentication that ensures that only authenticated users can listen on the private comment
channel we created.
Add the following code to the routes/channels.php
file:
1// File: ./routes/channels.php 2 Broadcast::channel('comment', function ($user) { 3 return auth()->check(); 4 });
In the second article of this series, we created the view for the single post landing page in the single.blade.php
file, but we didn’t add the comments functionality. We are going to add it now. We will be using Vue to build the comments for this application so the first thing we will do is include Vue in the frontend of our application.
Open the master layout template and include Vue to its <head>
tag. Just before the <title>
tag appears in the master.blade.php
file, include this snippet:
1<!-- File: ./resources/views/layouts/master.blade.php --> 2 <meta name="csrf-token" content="{{ csrf_token() }}"> 3 <script src="{{ asset('js/app.js') }}" defer></script>
The csrf_token()
is there so that users cannot forge requests in our application. All our requests will pick the randomly generated csrf-token
and use that to make requests.
Related: CSRF in Laravel: how VerifyCsrfToken works and how to prevent attacks
Now the next thing we want to do is update the resources/assets/js/app.js
file so that it includes a template for the comments view.
Open the file and replace its contents with the code below:
1require('./bootstrap'); 2 3 import Vue from 'vue' 4 import VueRouter from 'vue-router' 5 import Homepage from './components/Homepage' 6 import Create from './components/Create' 7 import Read from './components/Read' 8 import Update from './components/Update' 9 import Comments from './components/Comments' 10 11 Vue.use(VueRouter) 12 13 const router = new VueRouter({ 14 mode: 'history', 15 routes: [ 16 { 17 path: '/admin/dashboard', 18 name: 'read', 19 component: Read, 20 props: true 21 }, 22 { 23 path: '/admin/create', 24 name: 'create', 25 component: Create, 26 props: true 27 }, 28 { 29 path: '/admin/update', 30 name: 'update', 31 component: Update, 32 props: true 33 }, 34 ], 35 }); 36 37 const app = new Vue({ 38 el: '#app', 39 components: { Homepage, Comments }, 40 router, 41 });
Above we imported the Comment
component and then we added it to the list of components in the applications Vue instance.
Now create a Comments.vue
file in the resources/assets/js/components
directory. This is where all the code for our comment view will go. We will populate this file later on.
For us to be able to use Pusher and subscribe to events on the frontend, we need to pull in both Pusher and Laravel Echo. We will do so by running this command:
$ npm install --save laravel-echo pusher-js
Laravel Echo is a JavaScript library that makes it easy to subscribe to channels and listen for events broadcast by Laravel.
Now let’s configure Laravel Echo to work in our application. In the resources/assets/js/bootstrap.js
file, find and uncomment this snippet of code:
1import Echo from 'laravel-echo' 2 3 window.Pusher = require('pusher-js'); 4 5 window.Echo = new Echo({ 6 broadcaster: 'pusher', 7 key: process.env.MIX_PUSHER_APP_KEY, 8 cluster: process.env.MIX_PUSHER_APP_CLUSTER, 9 encrypted: true 10 });
The
key
andcluster
will pull the keys from your.env
file so no need to enter them manually again.
Now let’s import the Comments
component into the single.blade.php
file and pass along the required the props.
Open the single.blade.php
file and replace its contents with the code below:
1{{-- File: ./resources/views/single.blade.php --}} 2 @extends('layouts.master') 3 4 @section('content') 5 <div class="container"> 6 <div class="row"> 7 <div class="col-lg-10 mx-auto"> 8 <br> 9 <h3 class="mt-4"> 10 {{ $post->title }} 11 <span class="lead">by <a href="#">{{ $post->user->name }}</a></span> 12 </h3> 13 <hr> 14 <p>Posted {{ $post->created_at->diffForHumans() }}</p> 15 <hr> 16 <img class="img-fluid rounded" src="{!! !empty($post->image) ? '/uploads/posts/' . $post->image : 'http://placehold.it/750x300' !!}" alt=""> 17 <hr> 18 <div> 19 <p>{{ $post->body }}</p> 20 <hr> 21 <br> 22 </div> 23 24 @auth 25 <Comments 26 :post-id='@json($post->id)' 27 :user-name='@json(auth()->user()->name)'> 28 </Comments> 29 @endauth 30 </div> 31 </div> 32 </div> 33 @endsection
Open the Comments.vue
file and add the following markup template below:
1<template> 2 <div class="card my-4"> 3 <h5 class="card-header">Leave a Comment:</h5> 4 <div class="card-body"> 5 <form> 6 <div class="form-group"> 7 <textarea ref="body" class="form-control" rows="3"></textarea> 8 </div> 9 <button type="submit" @click.prevent="addComment" class="btn btn-primary"> 10 Submit 11 </button> 12 </form> 13 </div> 14 <p class="border p-3" v-for="comment in comments"> 15 <strong>{{ comment.user.name }}</strong>: 16 <span>{{ comment.body }}</span> 17 </p> 18 </div> 19 </template>
Now, we’ll add a script that defines two methods:
fetchComments()
- this will fetch all the existing comments when the component is created.addComment()
- this will add a new comment by hitting the backend server. It will also trigger a new event that will be broadcast so all clients receive them in realtime.In the same file, add the following below the closing template
tag:
1<script> 2 export default { 3 props: { 4 userName: { 5 type: String, 6 required: true 7 }, 8 postId: { 9 type: Number, 10 required: true 11 } 12 }, 13 data() { 14 return { 15 comments: [] 16 }; 17 }, 18 19 created() { 20 this.fetchComments(); 21 22 Echo.private("comment").listen("CommentSent", e => { 23 this.comments.push({ 24 user: {name: e.user.name}, 25 body: e.comment.body, 26 }); 27 }); 28 }, 29 30 methods: { 31 fetchComments() { 32 axios.get("/" + this.postId + "/comments").then(response => { 33 this.comments = response.data; 34 }); 35 }, 36 37 addComment() { 38 let body = this.$refs.body.value; 39 axios.post("/" + this.postId + "/comments", { body }).then(response => { 40 this.comments.push({ 41 user: {name: this.userName}, 42 body: this.$refs.body.value 43 }); 44 this.$refs.body.value = ""; 45 }); 46 } 47 } 48 }; 49 </script>
In the created()
method above, we first made a call to the fetchComments()
method, then we created a listener to the private comment
channel using Laravel Echo. Once this listener is triggered, the comments
property is updated.
Now let’s test the application to see if it is working as intended. Before running the application, we need to refresh our database so as to revert any changes. To do this, run the command below in your terminal:
$ php artisan migrate:fresh --seed
Next, let’s build the application so that all the changes will be compiled and included as a part of the JavaScript file. To do this, run the following command on your terminal:
$ npm run dev
Finally, let’s serve the application using this command:
$ php artisan serve
To test that our application works visit the application URL http://localhost:8000 on two separate browser windows, we will log in to our application on each of the windows as a different user.
We will finally make a comment on the same post on each of the browser windows and check that it updates in realtime on the other window:
In this final tutorial of this series, we created the comments feature of the CMS and also made it realtime. We were able to accomplish the realtime functionality using Pusher.
In this entire series, we learned how to build a CMS using Laravel and Vue.