tizis/lara-comments
tizis/lara-comments adds a lightweight comments system to Laravel apps, letting you attach comments to any Eloquent model via polymorphic relations and manage them through a simple, customizable API.
Install and publish
composer require tizis/lara-comments
php artisan vendor:publish --provider="Tizis\LaraComments\CommentsServiceProvider"
php artisan migrate
The publish step typically drops out a config file (HTMLPurifier settings, auth rules, route prefix) and the migrations for the comments table. Read the published config first — it is where every non-default behavior lives (sanitization, moderation, rating bounds, route registration). Most "the package doesn't work" issues are config, not code.
Wire up the traits
// app/Models/Post.php
use Tizis\LaraComments\Traits\Commentable;
class Post extends Model
{
use Commentable;
}
// app/Models/User.php
use Tizis\LaraComments\Traits\Commenter;
class User extends Authenticatable
{
use Commenter;
}
Commentable gives the model a comments() morph-many relation and helpers to create comments. Commenter gives a user their own authored comments and the ability to post/reply/rate.
First use case: comment on a Post
$post = Post::find(1);
$comment = $post->comment('Great write-up!'); // creates against current user
$comment = auth()->user()->comment($post, 'Nice'); // explicit commenter form
$post->comments; // top-level + all, depending on config
$post->comments()->count();
Blade rendering:
@foreach($post->comments as $comment)
<div class="comment">
<strong>{{ $comment->commenter->name }}</strong>
{!! $comment->body !!} {{-- already purified by the package --}}
@include('comments._replies', ['comments' => $comment->children])
</div>
@endforeach
Where to look first
config/lara-comments.php (or similarly named) — purifier profile, auth gates, nesting depth, route enable/disable.vendor/tizis/lara-comments/src/Models/Comment.php — the public methods here are the real API (reply(), rate(), children, parent, scopes).routes/ inside the package if you plan to use the API rather than hand-rolled endpoints.Polymorphic commenting across many models. The whole point of the package is that any model using Commentable can be commented on. In practice, register the trait on a shared base model or a small set of core resources (Post, Product, Ticket, Lesson) and do not add it to models you'll later want to comment on via a different type — morph aliases matter. If you use morph maps (Relation::enforceMorphMap), define aliases before the first comment is written, or you'll have a table full of FQCNs.
Threaded replies. Replies are parent-child via a parent_id (or morph equivalent). The daily pattern is a two-level UI: render roots, then $comment->children (or ->replies). For deeper threads either recursively include a partial or load the tree once:
$comments = $post->comments()
->whereNull('parent_id')
->with(['commenter', 'children.commenter', 'children.children.commenter'])
->latest()
->paginate(20);
Cap depth in config and enforce it on the server side too — a reply endpoint that trusts the client's parent_id will happily accept a 40-deep chain.
Ratings / upvotes. The package exposes rating as a column plus helper methods ($comment->rate(1), $comment->rate(-1), or similar depending on version). Two things to decide up front:
comment_ratings pivot) or users can spam ratings on reload.Events for notifications and moderation. The package fires create/update/delete/rate events. This is the cleanest integration point — don't override the model:
// app/Providers/EventServiceProvider.php
protected $listen = [
\Tizis\LaraComments\Events\CommentCreated::class => [
\App\Listeners\NotifyCommentSubscribers::class,
\App\Listeners\QueueModerationCheck::class,
],
];
Push the listener work onto a queue (ShouldQueue). Notification fan-out on a busy thread is the #1 latency source in comment sections.
Auth rules. Rather than sprinkling @can throughout Blade, centralize in a policy or the package's config-driven gate. Typical rules: authenticated to post, author to edit within N minutes, author or moderator to delete, author to edit body but not rating. If the package ships config booleans for these, prefer them over a parallel policy so the API and UI agree.
API usage. If the package registers routes, put them behind auth:sanctum (or auth:api) and your rate limiter:
Route::middleware(['auth:sanctum', 'throttle:comments'])->group(function () {
// package API routes, or your own controllers wrapping Comment::create()
});
For a SPA/mobile client, wrap the package in your own thin controller/Resource so you control the JSON shape and can version it. Coupling mobile clients directly to a 2023-era package's response format is how you get stuck.
HTML filtering customization. The package uses HTMLPurifier. The custom hook you'll actually use is tightening or loosening the allowed tags per context:
// config/lara-comments.php
'purifier' => [
'HTML.Allowed' => 'p,br,strong,em,a[href|title],code,pre,blockquote',
'AutoFormat.AutoParagraph' => true,
'URI.AllowedSchemes' => ['http' => true, 'https' => true, 'mailto' => true],
],
Define named profiles (e.g. strict for public blog comments, rich for internal tickets) and choose one per commentable type. Keep sanitization on input, then render with {!! !!} — don't rely on Blade escaping a value you also purified, or you'll double-encode entities.
Moderation workflow. A common pattern is to add a status column via your own migration on top of the package table (or use the package's if present): pending, approved, spam. Then a global scope or query scope on the Comment model filters unapproved comments out of public listings while admins see everything. Do this as a scope, not a trait override, so you can opt out in admin panels.
Maintenance is the headline risk. Last release is March 2023 and the repo has ~119 stars. Before adopting:
"tizis/lara-comments": "1.x") — never dev-master in production.Sanitization config is compiled. HTMLPurifier caches its serialized config in storage. If you change HTML.Allowed and see no effect, clear the package/config cache:
php artisan config:clear
php artisan cache:clear
rm -rf storage/framework/cache/* # or the configured purifier cache path
Also check that the purifier cache directory is writable — on read-only container filesystems it fails silently or throws deep in the library.
N+1 is the default failure mode. $comment->commenter and $comment->children are lazy relations. A 20-comment page with replies can easily become hundreds of queries. Always with() the commenter and the reply chain you render. Add a DB index on (commentable_type, commentable_id, parent_id, created_at) if you paginate by newest under a parent.
Morph integrity on delete. Deleting a Post does not necessarily delete its comments unless the migration defined onDelete('cascade') on the morph pair — and morph columns often can't have FK constraints. Either add a model observer that deletes comments when a commentable is deleted, or rely on a scheduled cleanup. Soft-deleting a comment also matters: does the UI show "[deleted]" with replies intact, or hide the whole subtree? Decide and implement explicitly; packages rarely do it for you.
Replies to deleted/hidden parents. Enforce the invariant that a reply's parent exists and is visible. Filtering only top-level comments in a scope will orphan visible replies to a hidden parent, producing broken thread UIs.
Rating counters drift. If the package maintains an aggregate rating column, concurrent votes can produce lost updates. Wrap vote logic in a transaction or use atomic increments (increment('rating')), and back it with a unique constraint on (comment_id, user_id) if you track per-user votes. Add a periodic reconciliation job if aggregates matter.
Auth checks must live server-side. Any "can comment" UI flag is cosmetic. Verify on the create/update/destroy path — including the API — that the current user is the author or a moderator. The package's config-level auth rules only apply to its own routes; your custom controllers must re-check.
Extension points to prefer, in order: events → config → model scopes → subclassing the Comment model (if the package resolves it from config). Overriding the model is the most fragile option across upgrades; prefer events and scopes.
Route collisions. If you publish the package routes under /api/comments and also have your own comments resource, Laravel will silently match one and shadow the other. Namespace package routes with a prefix and confirm with php artisan route:list.
Testing. Use RefreshDatabase and assert after the events fire — comment creation fires events, and a listener that throws will fail the test with a confusing stack trace. For the UI, test that sanitization actually strips a <script> payload end-to-end ($post->comment('<script>alert(1)</script>hi')) so a config regression gets caught.
How can I help you explore Laravel packages today?