How to build a live comment system with Laravel and Vue.js

December 30, 2021

Tags: Technologies
laravel
Unsplash

 

According to the definition given on their official website, Laravel “is a web application framework with an elegant and expressive syntax. We've already laid the groundwork, allowing you to create without worrying about the little things."

 

Among the functions that make Laravel a favorite framework among developers are:

 

  • The combination of the best packages in the PHP ecosystem offers a robust and developer-friendly framework.
  • The ability to create dynamic sites with an innovative templating engine.
  • A very safe and bulletproof migration process.
  • Pre-installed and goal-oriented libraries.

 

Vue, meanwhile, “is a progressive framework for creating user interfaces. Unlike other monolithic frameworks, Vue is designed from the ground up to be gradually adoptable. The main library focuses only on the view layer and is easy to collect and integrate with other existing libraries or projects ”, as defined on the official website.

 

Combined, Laravel and Vue can help developers create some parts of a web page, such as a dynamic comment section where the user can instantly type and view their message on the page.

 

How to create a comment section combining Laravel, Vue, and Pusher

 

First, you must make sure you have installed on your computer: Pusher, Laravel 5.7, Vue.js, Vuex, and Laravel CLI. And of course, knowing how to properly handle all these technologies.

 

We start by creating the project and installing the dependencies. First, install a new Laravel application using the Laravel CLI. Run the following command:

 

laravel new live_comments

 

When the installation is ready, run the following command to move to the application directory:

 

cd live_comments

 

The next step is to install the Node dependencies, first paste this code in your package.json file:

 

/live_comments/package.json
    {
      "private": true,
      "scripts": {
        "dev": "npm run development",
        "development": "cross-env NODE_ENV = development node_modules / webpack / bin / webpack.js --progress --hide-modules --config = node_modules / laravel-mix / setup / webpack.config.js",
        "watch": "npm run development - --watch",
        "watch-poll": "npm run watch - --watch-poll",
        "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 ",
        "prod": "npm run production",
        "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"
      },
      "devDependencies": {
        "axios": "^ 0.18",
        "bootstrap": "^ 4.0.0",
        "cross-env": "^ 5.1",
        "jquery": "^ 3.2",
        "laravel-mix": "^ 2.0",
        "lodash": "^ 4.17.5",
        "popper.js": "^ 1.12",
        "vue": "^ 2.5.7",
        "vuex": "^ 3.0.1",
        "moment": "^ 2.22.2",
        "pusher-js": "^ 4.2.2"
      }
    }

 

Then run npm install or yarn to install the dependencies, you decide.

 

Then add the following to your .env file at the root of your project directory. Make sure to replace all the placeholders with the Pusher Keys.

 

PUSHER_APP_ID = YOUR_PUSHER_APP_ID
    PUSHER_APP_KEY = YOUR_PUSHER_APP_KEY
    PUSHER_APP_SECRET = YOUR_PUSHER_APP_SECRET
    PUSHER_APP_CLUSTER = YOUR_PUSHER_APP_CLUSTER

 

Database configuration

 

We will use SQLite as our database. Create a database.sqlite file in the database directory and modify the .env file like this:

 

DB_CONNECTION = sqlite
DB_DATABASE = / absolute / path / to / database.sqlite

 

We are now going to build models and seed our database. Let's build our database structure. For this, we will use Laravel CLI again.

 

Run this command:

 

PHP artisan make: model Comment -mc

 

The above command will generate the Comment model, as well as its migration and its CommentController.php controller for us.

 

Open your Comment.php file and paste this:

 

//live_comments/app/Comment.php
    
    <? php
    
    namespace App;
    
    use Illuminate \ Database \ Eloquent \ Model;
    
    class Comment extends Model {
    
        //
        protected $ fillable = ['content', 'author'];
    }

 

Then copy and paste this piece of code into your comment migration file:

 

//live_comments/database/migrations/*_create_comments_table.php
    <? php
    
    use Illuminate \ Database \ Migrations \ Migration;
    use Illuminate \ Database \ Schema \ Blueprint;
    use Illuminate \ Support \ Facades \ Schema;
    
    class CreateCommentsTable extends Migration
    {
        / **
         * Run the migrations.
         *
         * @return void
         * /
       
        public function up ()
        {
            Schema :: create ('comments', function (Blueprint $ table) {
                $ table-> increments ('id');
                $ table-> string ('content');
                $ table-> string ('author');
                $ table-> timestamps ();
            });
        }
    
        / **
         * Reverse the migrations.
         *
         * @return void
         * /
         
        the public function down ()
        {
            Schema :: dropIfExists ('comments');
        }
    }

 

By doing this, you run PHP artisan migrate to run the migration.

 

Define the routes and create the CommentController

 

In this section, we are going to define the endpoints of the application and define the logic behind CommentController.php.

 

We start by creating three basic paths for our application, one that represents the view of the application, one to get comments from the database, and the last one that works to store comments in the database.

 

Paste the following in api.php:

 

//live_comments/routes/api.php
    <? PHP
    use Illuminate \ Support \ Facades \ Route;
    
    Route :: get ('/', 'CommentController @ index');
    
    Route :: prefix ('api') -> group (function () {
        Route :: get ('/ comments', 'CommentController @ fetchComments');
        Route :: post ('/ comments', 'CommentController @ store');
    });

And modify web.php like the following:

//live_comments/routes/web.php
    <? PHP
    use Illuminate \ Support \ Facades \ Route;
    Route :: get ('/', 'CommentController @ index');

 

The next thing is to define the logic of our controller. Our controller functions will be responsible for the actions to handle when some requests reach the API endpoints.

 

Open your CommentController file and paste the following code:

 

//live_comments/app/Http/Controllers/CommentController.php
    <? php
    
    namespace App \ Http \ Controllers;
    
    use App \ Comment;
    use App \ Events \ CommentEvent;
    use Illuminate \ Http \ Request;
    
    class CommentController extends Controller
    {
        //
    
        public function index ()
        {
    
            return view ('comments');
        }
    
        public function fetchComments ()
        {
            $ comments = Comment :: all ();
    
            return response () -> json ($ comments);
        }
    
        public function store (Request $ request)
        {
            $ comment = Comment :: create ($ request-> all ());
    
            event (new CommentEvent ($ comment));
            return response () -> json ('ok');
    
        }
    }

 

Create a search event with broadcast

 

Our SearchEvent event will be fired every time a user posts a comment. Let's create our CommentEvent by executing the following command in your terminal: PHP artisan make: event CommentEvent.

 

Now open your CommentEvent file and paste the following:

 

//live_comments/app/Events/CommentEvent.php
    <? php
    
    namespace App \ Events;
    
    use Illuminate \ Broadcasting \ Channel;
    use Illuminate \ Broadcasting \ InteractsWithSockets;
    use Illuminate \ Contracts \ Broadcasting \ ShouldBroadcastNow;
    use Illuminate \ Foundation \ Events \ Dispatchable;
    use Illuminate \ Queue \ SerializesModels;
    
    class CommentEvent implements ShouldBroadcastNow
    {
        use Dispatchable, InteractsWithSockets, SerializesModels;
    
        public $ comment;
    
        / **
         * Create a new event instance.
         *
         * @param $ comment
         *
         * @return void
         * /
         
        public function __construct ($ comment)
        {
            //
            $ this-> comment = $ comment;
        }
    
        / **
         * Get the channels the event should broadcast on.
         *
         * @return \ Illuminate \ Broadcasting \ Channel | array
         * /
    
        public function broadcastOn ()
        {
            return new Channel ('comment-channel');
        }
    
        public function broadcastAs ()
        {
            return 'newComment';
        }
    
    
    }

 

Stream settings

 

According to the Laravel documentation on broadcasting events, before starting broadcasting, you must first register App \ Providers \ BroadcastServiceProvider. When they are new Laravel applications, you need to uncomment this provider in the array of your config file ../config/app.php. This provider will allow you to record transmission authorization routes and callbacks.

 

If it does this, you must tell Laravel to use Pusher to broadcast events. Open your .env file and make sure you have this line: BROADCAST_DRIVER = pusher

 

While we are streaming our events through Pusher, install the Pusher PHP SDK using the Composer package manager:

 

composer require pusher / pusher-php-server "~ 3.0"

 

Configure the transmission channel

 

Paste the following code in your channels.php file:

 

    //live_comments/routes/channels.php
    Broadcast::channel('comment-channel', function () {
        return true;
    });
 

 

Since we don't use Laravel authentication, we return true in the callback function so everyone can use this channel to broadcast events.

Now modify your bootstrap.js file as follows:

 

//live_comments/resources/js/bootstrap.js
    
    window._ = require ('lodash');
    
    window.axios = require ('axios');
    window.moment = require ('moment')
    
    // import 'vue-tel-input / dist / vue-tel-input.css';
    
    window.axios.defaults.headers.common ['X-Requested-With'] = 'XMLHttpRequest';
    window.axios.defaults.headers.post ['Content-Type'] = 'application / x-www-form-urlencoded';
    window.axios.defaults.headers.common.crossDomain = true;
    window.axios.defaults.baseURL = '/ api';
    
    let token = document.head.querySelector ('meta [name = "csrf-token"]');
    
    if (token) {
        window.axios.defaults.headers.common ['X-CSRF-TOKEN'] = token.content;
    } else {
        console.error ('CSRF token not found: https://adonisjs.com/docs/4.1/csrf');
    }
    
    
    window.Pusher = require ('pusher-js');

 

Now, let's focus on the Frontend part of the application

 

Set up Vuex store

 

We will use the Vuex library to centralize our data and control how it is modified throughout the application.

 

Create Our State

 

A Vuex state is a single object that contains all the data for our application. So let's create ../resources/js/store/state.js and paste this code inside:

 

let state = {
        comments: []
    }
    export default state

Getters

Create ../resources/js/store/getters.js and paste this code inside

    let getters = {
        comments: state => {
            return state.comments
        }
    }
    
    export default getters

 

Mutations

 

Create ../resources/js/store/mutations.js and paste this code inside:

 

let mutations = {
      GET_COMMENTS (state, comments) {
        state.comments = comments
      },
      ADD_COMMENT (state, comment) {
        state.comments = [... state.comments, comment]
      }
    }
    
    export default mutations

 

Actions

 

Create the file ../resources/js/store/actions.js and paste the following code:

 

let actions = {
      ADD_COMMENT ({commit}, comment) {
    
        return new Promise ((resolve, reject) => {
          axios.post (`/ comments`, comment)
            .then (response => {
              resolve (response)
            }). catch (err => {
            reject (err)
          })
        })
      },
    
      GET_COMMENTS ({commit}) {
        axios.get ('/ comments')
          .then (res => {
            {
              commit ('GET_COMMENTS', res.data)
            }
          })
          .catch (err => {
            console.log (err)
          })
      }
    }
    
    export default actions

 

Set up the store with Vue

 

Create the file ../resources/js/store/index.js and paste the code:

 

import Vue from 'vue'
    import Vuex from 'vuex'
    import actions from './actions'
    import mutations from './mutations'
    import getters from './getters'
    import state from "./state";
    
    Vue.use (Vuex);
    
    export default new Vuex.Store ({
        state,
        mutations,
        getters,
        actions
    })

 

Create the Comment.vue component

 

The Comment.Vue component is responsible for encapsulating the details about a single comment instance in the database and rendering it appropriately and stylishly. Paste the following into the Comment.Vue component.

 

//../resources/js/components/Comment.vue
    
    <template>
      <li class = "comment-wrapper animate slideInLeft">
        <div class = "profile">
          <img: src = "avatar" alt = ""> </div>
        <div class = "msg has-shadow">
          <div class = "msg-body"> <p class = "name"> {{comment.author}} <span class = "date"> {{posted_at}} </span> </p>
            <p class = "content"> {{comment.content}} </p> </div>
        </div>
      </li>
    </template>
    
    <script>
      export default {
        name: "Comment",
        props: ['comment'],
        computed: {
          posted_at () {
            return moment (this.comment.created_at) .format ('MMMM Do YYYY')
          },
          avatar () {
            return `https://api.adorable.io/avatars/48/$ {this.comment.author} @ adorable.io.png`
          }
        }
      }
    </script>
    
    <style lang = "scss" scoped>
      .comment-wrapper {
        list-style: none;
        text-align: left;
        overflow: hidden;
        margin-bottom: 2em;
        padding: .4em;
    
        .profile {
          width: 80px;
          float: left;
        }
    
        .msg-body {
          padding: .8em;
          color: # 666;
          line-height: 1.5;
        }
    
        .msg {
          width: 86%;
          float: left;
          background-color: #fff;
          border-radius: 0 5px 5px 5px;
          position: relative;
          &::despues de {
            content: "";
            position: absolute;
            left: -13px;
            top: 0;
            border: 14px solid transparent;
            border-top-color: #fff;
          }
        }
    
        .date {
          float: right;
        }
        .name {
          margin: 0;
          color: # 999;
          font-weight: 700;
          font-size: .8em;
        }
    
        p: last-child {
          margin-top: .6em;
          margin-bottom: 0;
        }
     
      }
    
    
    </style>

 

Create the Comments component. vue

 

Create the component and paste the following code:

 

../resources/js/components/Comments.vue
    
    <template>
      <div class = "container">
        <ul class = "comment-list">
          <Comment: key = "comment.id" v-for = "comment in comments": comment = "comment"> </Comment>
        </ul>
      </div>
    </template>
    
    <script>
      import {mapGetters} from 'vuex'
      import Comment from './Comment'
    
      export default {
        name: "Comments",
        components: {Comment},
        mounted () {
          this. $ store.dispatch ('GET_COMMENTS')
    
         // use your own credentials you get from Pusher
          let pusher = new Pusher (`YOUR_PUSHER_APP_ID`, {
            cluster: `YOUR_PUSHER_CLUSTER`,
            encrypted: false
          });
    
          // Subscribe to the channel we specified in our Adonis Application
          let channel = pusher.subscribe ('comment-channel')
    
          channel.bind ('new-comment', (data) => {
            this. $ store.commit ('ADD_COMMENT', data.comment)
          })
        },
        computed: {
          ... mapGetters ([
            'comments'
          ])
        }
      }
    </script>
    
    <style scoped>
      .comment-list {
        padding: 1em 0;
        margin-bottom: 15px;
      }
    
    </style>

 

Create the NewComment.vue component. Create it and paste the following code:

 

../resources/js/components/NewComment.vue
    <template>
      <div id = "commentForm" class = "box has-shadow has-background-white">
    
        <form @ keyup.enter = "postComment">
          <div class = "field has-margin-top">
    
            <div class = "field has-margin-top">
              <label class = "label"> Your name </label>
              <div class = "control">
                <input type = "text" placeholder = "Your name" class = "input is-medium" v-model = "comment.author">
              </div>
    
            </div>
            <div class = "field has-margin-top">
              <label class = "label"> Your comment </label>
              <div class = "control">
                            <textarea
                              style = "height: 100px;"
                              name = "comment"
                              class = "input is-medium" autocomplete = "true" v-model = "comment.content"
                              placeholder = "lorem ipsum"> </textarea>
              </div>
    
            </div>
            <div class = "control has-margin-top">
              <button style = "background-color: # 47b784": class = "{'is-loading': submit}"
                      class = "button has-shadow is-medium has-text-white"
                      : disabled = "! isValid"
                      @ click.prevent = "postComment"
                      type = "submit"> Submit
              </button>
            </div>
          </div>
        </form>
        for
      </div>
    </template>
    
    <script>
      export default {
        name: "NewComment",
        data() {
          return {
            submit: false,
            comment: {
              content: '',
              author: '',
            }
          }
        },
        methods: {
          postComment () {
            this.submit = true;
            this. $ store.dispatch ('ADD_COMMENT', this.comment)
              .then (response => {
                this.submit = false;
                if (response.data === 'ok')
                  console.log ('success')
              }). catch (err => {
              this.submit = false
            })
    
          },
        },
        computed: {
          isValid () {
            return this.comment.content! == '' && this.comment.author! == ''
          }
        }
      }
    </script>
    
    <style scoped>
      .has-margin-top {
        margin-top: 15px;
      }
    
    </style>

 

End the application

 

Now, let's create our comments.blade.php file that contains our Vue.js components. Then paste this code inside:

 

//live_comments/resources/views/comments.blade.php
    
    <! DOCTYPE html>
    <html lang = "en">
    <head>
        <meta charset = "UTF-8" />
        <title> Live commenting system with Laravel and Pusher </title>
        <meta name = "csrf-token" content = "{{csrf_token ()}}">
        <meta name = "viewport"
              content = "width = device-width, user-scalable = no, initial-scale = 1.0, maximum-scale = 1.0, minimum-scale = 1.0">
    
        <! - Bootstrap core CSS ->
        <link rel = "stylesheet" href = "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css" />
        <link rel = "stylesheet" href = "https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css" />
    
        <style>
            html {
                background: radial-gradient (ellipse at center, #fff 0, #ededfd 100%);
            }
    
            #app {
                width: 60%;
                margin: 4rem auto;
            }
    
            .container {
                margin: 0 auto;
                position: relative;
                width: unset;
            }
    
            .question-wrapper {
                text-align: center;
            }
    
            .has-shadow {
                box-shadow: 0 4px 8px -2px rgba (0, 0, 0, 0.05)! important;
            }
    
        </style>
    </head>
    <body>
    
    
    <div id = "app">
    
        <div class = "container">
            <div class = "question-wrapper">
                <img width = "200" src = "{{asset ('images / adonuxt.png')}}" alt = "">
                <h5 class = "is-size-2" style = "color: # 220052;">
                    What do you think about <span style = "color: # 47b784;"> Laravel </span>? </h5>
                for
                <a href = "# commentForm" class = "button is-medium has-shadow has-text-white" style = "background-col
                or: # 47b784 "> Comment </a>
            </div>
    
            <br> <br>
            <comments> </comments>
            <new-comment> </new-comment>
        </div>
    </div>
    
    <script async src = "{{mix ('js / app.js')}}"> </script>
    
    </body>
    
    </html>

 

Almost there! Open your terminal and run npm run dev to build the application. This may take a few seconds. After this step, run PHP artisan serve and open your browser to localhost: 8000 to certify that the application works fine.

 

We recommend you on video

 

Let's work together!