← Back to fiachehr.ir
Fiachehr · Package docs

Laravel Comments Pro

Production-oriented comments for Laravel: polymorphic commentables, nested threads, like/dislike reactions, guest comments with fingerprinting, moderation (approve/pending), events, and a JSON-friendly tree helper. Supports Laravel 10, 11, and 12.

Features

  • Nested comments — configurable max depth; reply with parent_id.
  • Reactions — like / dislike with toggle, bulk toggle, per-comment stats, “popular” queries.
  • Guests — optional guest name/email; fingerprint rule for cookies.
  • Moderation — status workflow; approved() scope on queries.
  • Tree outputComments::toTree() for hierarchical JSON/API responses.
  • Polymorphic — any Eloquent model can use HasComments.

Requirements

  • PHP ≥ 8.1 (PHP 8.2+ required for Laravel 11 and 12).
  • Laravel 10.x, 11.x, or 12.x.
  • Composer.

The package is Laravel-only (Eloquent, service container, events, migrations, Artisan). It is not usable outside the Laravel ecosystem.

Compatibility matrix

PHP Laravel 10 Laravel 11 Laravel 12
8.1
8.2+

Installation

composer require fiachehr/laravel-comments-pro

Migrations

php artisan vendor:publish --provider="Fiachehr\Comments\CommentsServiceProvider" --tag=comments-migrations
php artisan migrate

Configuration

php artisan vendor:publish --provider="Fiachehr\Comments\CommentsServiceProvider" --tag=comments-config

Optional publish tags

The provider may also register:

  • comments-requests — Form request stubs into app/Http/Requests.
  • comments-controllers / comments-routes — only if stub directories ship with your package version.

Quick start

1. Commentable models

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Fiachehr\Comments\Traits\HasComments;

class Post extends Model
{
    use HasComments;
}

class Article extends Model
{
    use HasComments;
}

2. Create comments

use Fiachehr\Comments\Facades\Comments;

$post = Post::find(1);

// Authenticated user (set user_id in your service layer if needed)
$comment = Comments::create([
    'body' => 'This is a great post!',
], $post);

// Guest
Comments::create([
    'body' => 'Nice article!',
    'guest_name' => 'John Doe',
    'guest_email' => 'john@example.com',
], $post);

// Nested reply
$reply = Comments::create([
    'body' => 'Thanks!',
    'parent_id' => $comment->id,
], $post);

3. Approve

use Fiachehr\Comments\Facades\Comments;

Comments::approve($comment);

4. Reactions

use Fiachehr\Comments\Facades\Reactions;
use Fiachehr\Comments\Enums\ReactionType;

Reactions::toggle($comment, ReactionType::LIKE);
Reactions::toggle($comment, ReactionType::DISLIKE);

5. Approved list → tree

$comments = $post->comments()->approved()->get();
$tree = Comments::toTree($comments);

6. Bulk reactions & stats

$results = Reactions::bulkToggle([1, 2, 3], ReactionType::LIKE);
$stats = Reactions::getStats($comment);
// e.g. ['likes' => 5, 'dislikes' => 2, 'total' => 7]

Configuration

File: config/comments.php (after publishing).

return [
    'route_prefix' => 'api/comments',
    'middleware' => ['api', 'throttle:60,1'],
    'max_depth' => 5,
    'auto_approve_authenticated' => true,
    'reply_only_to_approved_parent' => true,

    'guests' => [
        'allowed' => true,
        'require_email' => true,
        'cookie_name' => 'guest_fingerprint',
    ],
];
  • route_prefix / middleware — reserved for wiring API routes in your app.
  • max_depth — nesting limit enforced in the service layer.
  • auto_approve_authenticated — skip moderation for logged-in users when enabled.
  • reply_only_to_approved_parent — block replies under pending parents.
  • guests.* — guest policy and cookie name for fingerprint validation.

There is no per-model override on the trait; use env-specific config files or config() overrides.

Facades & services

Comments facade → CommentsService

MethodDescription
create(array $data, Model $commentable)Create comment; supports parent_id, guest fields.
approve(Comment $comment)Mark approved.
toTree(Collection $comments)Nested array structure for APIs.

Reactions facade → ReactionService

MethodDescription
toggle(Comment, ReactionType, ?guestFp, ?userId)Toggle like/dislike.
remove(Reaction)Remove a reaction row.
getStats(Comment)Aggregated counts.
bulkToggle(array $ids, ReactionType, …)Many comments at once.
getPopular($limit, $period)Facade alias for trending comments.

Direct service resolution

use Fiachehr\Comments\Services\CommentsService;
use Fiachehr\Comments\Services\ReactionService;

$commentsService = app(CommentsService::class);
$reactionService = app(ReactionService::class);

$comment = $commentsService->createComment($data, $post);
$reaction = $reactionService->toggleReaction($comment, ReactionType::LIKE);
$popular = $reactionService->getPopularComments(10, '7 days');

Eloquent scopes (on Comment via relation)

$post->comments()->approved()->get();
$post->comments()->withReactions()->get(); // loads reactions + like/dislike counts

Events

Shipped event classes (under Fiachehr\Comments\Events):

  • CommentCreated — fired when a new comment is created; exposes the Comment model.
  • ReactionToggled — carries the affected Reaction model ($event->reaction).
use Illuminate\Support\Facades\Event;
use Fiachehr\Comments\Events\CommentCreated;
use Fiachehr\Comments\Events\ReactionToggled;

Event::listen(CommentCreated::class, function (CommentCreated $event) {
    // $event->comment
});

Event::listen(ReactionToggled::class, function (ReactionToggled $event) {
    // $event->reaction
});

Register listeners in App\Providers\EventServiceProvider or bootstrap/app.php (Laravel 11+) as you prefer.

HTTP layer example

Controller

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StoreCommentRequest;
use Fiachehr\Comments\Models\Comment;
use Fiachehr\Comments\Services\CommentsService;
use Fiachehr\Comments\Services\ReactionService;
use Fiachehr\Comments\Enums\ReactionType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Routing\Controller;

class CommentController extends Controller
{
    public function __construct(
        private CommentsService $commentsService,
        private ReactionService $reactionService
    ) {}

    public function store(StoreCommentRequest $request, Model $commentable)
    {
        $comment = $this->commentsService
            ->createComment($request->validated(), $commentable);

        return response()->json([
            'success' => true,
            'comment' => $comment,
        ]);
    }

    public function approve(Comment $comment)
    {
        $approved = $this->commentsService->approveComment($comment);

        return response()->json([
            'success' => true,
            'comment' => $approved,
        ]);
    }

    public function react(Comment $comment, string $type)
    {
        $reactionType = ReactionType::from($type);
        $reaction = $this->reactionService
            ->toggleReaction($comment, $reactionType);

        return response()->json([
            'success' => true,
            'reaction' => $reaction,
        ]);
    }

    public function tree(Model $commentable)
    {
        $comments = $commentable->comments()->approved()->get();
        $tree = $this->commentsService->toTree($comments);

        return response()->json($tree);
    }
}

Form request

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Fiachehr\Comments\Rules\GuestFingerprint;

class StoreCommentRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'body' => 'required|string|max:1000',
            'parent_id' => 'nullable|exists:comments,id',
            'guest_name' => 'required_if:user_id,null|string|max:255',
            'guest_email' => 'required_if:user_id,null|email|max:255',
            'guest_fingerprint' => ['nullable', new GuestFingerprint()],
        ];
    }
}

Performance & caching

Eager loading

$comments = $post->comments()
    ->with(['user', 'reactions', 'children'])
    ->approved()
    ->get();

Cache tree or popular lists

use Illuminate\Support\Facades\Cache;

$tree = Cache::remember("comments_tree_{$post->id}", 1800, function () use ($post) {
    return Comments::toTree(
        $post->comments()->approved()->get()
    );
});

Security

  • Use Laravel’s CSRF + session or token auth on web/API routes you expose.
  • Throttle comment/reaction endpoints (see default middleware in config).
  • Validate all input via Form Requests; use GuestFingerprint when allowing guests.
  • Eloquent parameter binding protects against SQL injection for normal usage.

Frontend (Vue example)

Assume JSON tree from tree() and POST endpoints for store/react.

<template>
  <div class="comments">
    <div v-for="c in comments" :key="c.id">
      <p>{{ c.body }}</p>
      <button type="button" @click="react(c, 'like')">
        👍 {{ c.likes ?? 0 }}
      </button>
      <button type="button" @click="react(c, 'dislike')">
        👎 {{ c.dislikes ?? 0 }}
      </button>
      <CommentList v-if="c.children?.length" :comments="c.children" />
    </div>
  </div>
</template>

<script setup>
async function react(comment, type) {
  await $fetch(`/api/comments/${comment.id}/react`, {
    method: 'POST',
    body: { type },
  });
}
</script>
Nuxt / portfolio: align route_prefix and middleware with your API client (cookies, Sanctum, or Bearer tokens).

Testing

# From package clone
./vendor/bin/phpunit tests/Unit/

In a host Laravel app, add a testsuite in phpunit.xml pointing at the vendor tests if you want php artisan test to run them:

<testsuite name="Comments">
    <directory suffix="Test.php">./vendor/fiachehr/laravel-comments-pro/tests/Unit</directory>
</testsuite>
php artisan test --testsuite=Comments

Troubleshooting

  • Migration errorsphp artisan config:clear then re-run migrations; avoid duplicate publishes.
  • Guest validation — ensure guest_email / guest_name rules match config('comments.guests').
  • Reactions not updating UI — prefer withReactions() or refresh counts from API after toggle.
  • Route 404 — this package does not auto-register HTTP routes; define routes that call your controllers.