User commenting and notifications

Written by Riari 10 months ago.

Laravel-5

Adding user commenting capability to models using slynova/laravel-commentable is very simple, although it doesn't provide a controller, routes and views out of the box.

In this post, I'll detail my approach to writing those in a generic way such that comments can be easily enabled for a given model. I'll also cover Notifynder integration for notifying users of new comments made on their content.

Please note that the code samples given here are from a recent project I completed and as such may contain references to things that don't exist in a fresh Laravel 5.2 project (such as the Notification facade, which is from this package). I've modified them to remove any arbitrary code and simplify them where possible, but don't expect to drop them into your project without it breaking!

Routes

// Comments
$r->group(['prefix' => 'comments', 'as' => 'comment.'], function ($r) {
    $r->post(
        '{model}/{id}',
        ['as' => 'store', 'uses' => 'CommentController@store']
    );
    $r->get(
        '{comment}/edit',
        ['as' => 'edit', 'uses' => 'CommentController@edit']
    );
    $r->patch(
        '{comment}',
        ['as' => 'update', 'uses' => 'CommentController@update']
    );
    $r->delete(
        '{comment}',
        ['as' => 'delete', 'uses' => 'CommentController@delete']
    );
});

Controller

<?php namespace App\Http\Controllers;

use Auth;
use Illuminate\Http\Request;
use Notification;
use Notifynder;
use Slynova\Commentable\Models\Comment;

class CommentController extends Controller
{
    /**
     * Add a new comment.
     *
     * @param  Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $this->validate($request, [
            'model'         => 'in:UserProfile,Event',
            'id'            => 'integer',
            'body'          => 'required|min:3',
            'redirect_to'   => 'url',
        ]);

        $model = $this->resolve($request->route('model'), $request->route('id'));
        $this->authorize('addComment', $model);

        $model->comments()->create([
            'user_id' => Auth::id(),
            'body' => $request->input('body')
        ]);

        Notifynder::category('comment.added')
                   ->from($comment->user_id)
                   ->to($model->user_id)
                   ->url("{$model->url}#comments")
                   ->extra(['model_name' => $model->friendlyName])
                   ->send();

        Notification::success("Comment added.");

        return redirect($request->input('redirect_to'));
    }

    /**
     * Show the comment edit form.
     *
     * @param  Comment  $comment
     * @return \Illuminate\Http\Response
     */
    public function edit(Comment $comment)
    {
        $this->authorize($comment);
        return view('comment.edit', compact('comment'));
    }

    /**
     * Update a comment.
     *
     * @param  Comment  $comment
     * @param  Request  $request
     * @return \Illuminate\Http\Response
     */
    public function update(Comment $comment, Request $request)
    {
        $this->authorize('edit', $comment);
        $this->validate($request, [
            'body'          => 'required|min:3',
            'redirect_to'   => 'url',
        ]);

        $comment->body = $request->input('body');
        $comment->save();

        Notification::success("Comment updated.");

        return redirect($request->input('redirect_to'));
    }

    /**
     * Delete a comment.
     *
     * @param  Comment  $comment
     * @return \Illuminate\Http\Response
     */
    public function delete(Comment $comment)
    {
        $this->authorize($comment);

        $comment->delete();

        Notification::success("Comment removed.");

        return redirect()->back();
    }

    /**
     * Resolve a resource using the given model name and ID.
     *
     * @param  string  $model
     * @param  int  $id
     * @return \Illuminate\Database\Eloquent\Model
     */
    private function resolve($model, $id)
    {
        $class = "\App\Models\\{$model}";
        return (new $class)->findOrFail($id);
    }
}

A few things to note about the controller:

  • You can limit which models comments can be added to (pre-authorisation) by listing them in the validation rule for model: 'model' => 'in:UserProfile,Event'
  • As per Notifynder's docs, you need to define notification categories before the package can be used. In this controller, I use just one category named comment.added, which I generated using the command php artisan notifynder:create:category "comment.added" "{from.name} left a comment on your {extra.model_name}"
  • This controller assumes models have a few extra properties on them, namely url, which would contain a URL to view the model, and friendlyName, which would contain a user-friendly version of the model class name (e.g. 'User Profile' instead of 'UserProfile')

Views

resources/views/comment/partials/list.blade.php

<div id="comments" class="row">
    <div class="col s12">
        @if (!$commentPaginator->isEmpty())
            @foreach ($commentPaginator->items() as $comment)
                @include('comment.partials.row')
            @endforeach
            @include('partials.pagination', ['paginator' => $commentPaginator])
        @else
            <p class="grey-text center-align">
                No comments yet.
            </p>
        @endif
    </div>
</div>

resources/views/comment/partials/row.blade.php

<div class="row">
    <div class="col s12">
        <div class="card">
            <div class="card-content">
                {{ $comment->body }}
            </div>
            <div class="card-footer grey-text">
                <div class="pull-right">
                    @can ('edit', $comment)
                        <a href="{{ route('comment.edit', compact('comment')) }}">Edit</a>
                    @endcan
                    @can ('delete', $comment)
                        @include('partials.delete-link', ['action' => route('comment.delete', compact('comment')), 'text' => "Are you sure you want to delete {$comment->user->name}'s comment?"])
                    @endcan
                </div>
                <a href="{{ $comment->user->profile->url }}">
                    @include('user.partials.avatar', ['user' => $comment->user, 'class' => 'tiny circular'])
                    {{ $comment->user->name }}
                </a>
                {{ $comment->created_at->diffForHumans() }}
                @if ($comment->created_at != $comment->updated_at)
                    (edited {{ $comment->updated_at->diffForHumans() }})
                @endif
            </div>
        </div>
    </div>
</div>

resources/views/comment/partials/add.blade.php

@if (Auth::check())
    <p class="center-align">
        <a class="waves-effect waves-light btn btn-large modal-trigger" href="#add-comment">Add comment</a>
    </p>

    <div id="add-comment" class="modal bottom-sheet">
        <div class="modal-content">
            <h4>Add a comment</h4>

            <div class="row">
                <div class="col s12 m8 l8 offset-m2 offset-l2">
                    <form method="POST" action="{{ route('comment.store', compact('model', 'id')) }}">
                        {!! csrf_field() !!}

                        <div class="row">
                            <div class="input-field col s12">
                                <textarea id="body" name="body" class="materialize-textarea">{{ old('body') }}</textarea>
                            </div>
                        </div>
                        <div class="row">
                            <div class="input-field col s12 right-align">
                                <input type="hidden" name="redirect_to" value="{{ Request::url() }}#comments">
                                <a href="#!" class="modal-action modal-close waves-effect waves-green btn-large btn-flat">Cancel</a>
                                <button type="submit" class="waves-effect waves-light btn-large">
                                    Add
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
@endif

resources/views/comment/edit.blade.php

@extends('app')

@section('title', 'Edit comment')

@section('content')
<div class="row">
    <div class="col m6 offset-m3">
        <form method="POST" action="{{ route('comment.update', compact('comment')) }}">
            {!! csrf_field() !!}
            {!! method_field('PATCH') !!}

            <div class="row">
                <div class="input-field col s12">
                    <textarea id="body" name="body" class="materialize-textarea">{{ !is_null(old('body')) ? old('body') : $comment->body }}</textarea>
                </div>
            </div>

            <div class="row">
                <div class="input-field col s12 right-align">
                    <input type="hidden" name="redirect_to" value="{{ URL::previous() }}#comments">
                    <button type="submit" class="waves-effect waves-light btn-large">
                        Save
                    </button>
                </div>
            </div>
        </form>
    </div>
</div>
@endsection

Usage

@include('comment.partials.add', ['model' => 'Character', 'id' => $character->id])
@include('comment.partials.list')

And that's it. Commenting can easily be enabled for any model with three steps, and user notifications will Just Work™:

  • Add the Slynova\Commentable\Traits\Commentable trait to the model
  • Include the comment partials in the model's show view (as above)
  • List the model class name in the model validation rule in the CommentController::store() method