Start learning today

Choose Your Plan

Build real life apps. Learn by creating.

Pay Monthly

12

Pay Yearly

10

Build nested commenting system using Laravel and VueJs - Part 1

In this tutorial will learn how to build nested comments or threaded comments or recursive comments using Laravel and VueJs.

In this tutorial will cover the following topics:

In the part 1 of this tutorial will cover

  1. Creating new laravel project and basic setup
  2. Creating models and migrations
  3. Creating controllers
  4. Using passport for api authentication

in the part 2 and 3 of this tutorial will cover

  1. Creating vue components for our post and comments

# Creating new laravel project and basic setup

Laravel utilizes Composer to manage its dependencies, meaning you can install any laravel package using composer and hence its better to setup composer first.

If you are on windows machine download composer and run Composer-Setup.exe and you are done.

If you are on ubuntu then run the following commands in your terminal

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('SHA384', 'composer-setup.php') === '55d6ead61b29c7bdee5cccfb50076874187bd9f21f65d8991d46ec5cc90518f447387fb9f76ebae1fbbacf329e583e30') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

Once you have installed composer test it by typing composer in your terminal. If you are not getting any errors means your composer is ready to use, there are two options to install laravel, either by using laravel/installer or via Composer create-project command.

via laravel installer: open your terminal and type

composer global require "laravel/installer"
laravel new project-name

via composer create-project: open your terminal and type

composer create-project --prefer-dist laravel/laravel project-name

For example: let's create a project called recursive-comments and setup database and authentication. In your terminal type

laravel new recursive-comments
cd recursive-comments
php artisan make:auth

Once you have generated authentication scafoldings let's create a database and connect to it. In your terminal type, if you have a password then only type -p else just type mysql -u root

mysql -u root -p
password: enter-your-password
mysql> create database comments;
Query OK, 1 row affected (0.00 sec)

one last step is to update your .env file, find the following lines and update to match your database settings, I am using mysql and running on port 3306 at localhost, and we just created a database called comments and username is root and even the password for my localsystem is root

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=comments
DB_USERNAME=root
DB_PASSWORD=root

test your database connection by typing following commands inside your project directory

cd recursive-comments
php artisan migrate

you should get output similar to the following.

Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table

# Creating models and migrations

In our next step will create two database migrations and two models called Post and Comment to store our posts and comments. In your terminal type the following artisan commands

php artisan make:model Post -m
php artisan make:model Comment -m

this will create a model inside your app directory and also create corresponding migrations in your database/migrations folder. go to your create_posts_table migration and edit it

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->unsigned();
            $table->text('content');
            $table->timestamps();
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

now go to create_comments_table migration and edit it

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateCommentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->index();
            $table->integer('post_id')->index();
            $table->integer('parent_id')->index()->nullable();
            $table->text('content');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('comments');
    }
}

to keep it very simple, we are just allowing user to post only text and user can comment on these post or reply to the comments. In our comment's table we have parent_id which is nullable, meaning we just have one table to hold all comments with parent_id, when the parent_id is null means its a root comment and not a reply to anything, if our parent_id has a value then it is a reply to the comment with that parent_id. For example consider the following data in table. In this example, user with id 1 has commented on a post with id 1 and the content is hey there!, and in the second row an user with id 2 has replied to this comment saying hello!. hence the first comment has parent_id null and the second has parent_id 1. Now let's migrate these two tables, go to terminal and type the following

php artisan migrate

and you should get an output similar to this

Migrated: 2017_02_07_174102_create_posts_table
Migrated: 2017_02_07_174112_create_comments_table

Now let's setup our Post and Comment models with relations and to get threaded comments.

Go to your Post model under app directory and edit it.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = ['user_id', 'content'];

    public function user(){
        return $this->belongsTo(User::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    public function getThreadedComments(){
        return $this->comments()->with('user')->get()->threaded();
    }

    public function addComment($attributes)
    {
        $comment = (new Comment())->forceFill($attributes);
        $comment->user_id = auth()->id();
        return $this->comments()->save($comment);
    }
}

In this model we have a relationship between user and the post, since every post is created by user they belong to them. And the second relation is comments, a post can have many comments, and since each comments can have replies we are defining one more function called getThreadedComments which will get replies to those comments along with the user. And finally we are adding a method addComment which will insert a new comment to that post.

Now lets edit our Comment model

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    protected $fillable = ['content'];

    public function user()
    {
        return $this->belongsTo(User::class, 'user_id');
    }

    public function newCollection(array $models = [])
    {
        return new CommentCollection($models);
    }
}

Here we are again defining a relation between user and a comment and a newCollection method which will return collection of comments grouped by parent_id. Now lets create a CommentCollection class, create it under app directory.

<?php

namespace App;

use Illuminate\Database\Eloquent\Collection;

class CommentCollection extends Collection
{
    public function threaded()
    {
        $comments = parent::groupBy('parent_id');
        if (count($comments)) {
            $comments['root'] = $comments[''];
            unset($comments['']);
        }
        return $comments;
    }
}

# Creating controllers

In this step will create two resource controllers inside Api directory, go to terminal inside your project directory and type

php artisan make:controller Api/PostController --resource
php artisan make:controller Api/CommentController --resource

Now you will have two controllers created inside app/Http/Controllers/Api named PostController and CommentController

# Using passport for api authentication

You can study more about passport here. Passport is a laravel package and to use it we need to install it using composer, go to your project directory and type

composer require laravel/passport

you should get an output similar to the following go to config/app.php and add Laravel\Passport\PassportServiceProvider::class inside your providers array and then run php artisan migrate, this will create oauth2 tables for you. After running php artisan migrate in your terminal you should get an output similar to this.

Migrated: 2016_06_01_000001_create_oauth_auth_codes_table
Migrated: 2016_06_01_000002_create_oauth_access_tokens_table
Migrated: 2016_06_01_000003_create_oauth_refresh_tokens_table
Migrated: 2016_06_01_000004_create_oauth_clients_table
Migrated: 2016_06_01_000005_create_oauth_personal_access_clients_table

Now that we have setup tables, we need to create encryption keys, which will be used by laravel to create secure access tokens. to create keys in your terminal just type

php artisan passport:install

After running this command go to App/User.php and then add HasApiTokens trait.

<?php

namespace App;

use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];
}

next go to AuthServiceProvider and add Passport::routes(); inside boot function

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();
        Passport::routes();
    }
}

Finally, go to config/auth.php edit api array driver and change it from token to passport

'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],
    ],

One last step so that passport will take care of sending tokens for every request that we make.

Go to App\Http\Kernal.php and add \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class to web middleware

protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
             \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],
    ];

Part 2