Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nested Comments Laravel Package

coolsam/nested-comments

A Laravel package for threaded, nested comments. It ships models, migrations, and helpers for building and querying comment trees, making it easy to add multi-level replies to any Eloquent model with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Read the source before you install. With 0 stars and 0 dependents you are the first production user. Spend 30 minutes in the repo in this order:

  • composer.json → the filament/filament and php constraints. This is the real compatibility gate. If your panel is on Filament 4 and the constraint says ^3.0, stop here — the decision is fork-vs-build, not integration.
  • database/migrations/ → the comments table. Look specifically at parent_id, the morph columns (commentable_type/commentable_id) and their column types, and whether softDeletes exists.
  • src/Models/Comment.php → the fillable/guarded list and the children/parent relationships.
  • src/ components (form, infolist, table) → what the public API actually exposes, and whether authorization is delegated to policies or hardcoded.

Install and publish so the schema becomes app-owned.

composer require coolsam/nested-comments:1.x.y   # pin exact, not ^1.x
php artisan vendor:publish --provider="Coolsam\NestedComments\NestedCommentsServiceProvider"
php artisan migrate

Publish everything — migrations, config, views. Renaming the migration timestamp to your own convention is worth the two minutes: it makes the table unambiguously yours to evolve when the package goes quiet.

Attach the trait to the commentable model (e.g. Ticket), and register the plugin on the panel provider:

// app/Models/Ticket.php
use Coolsam\NestedComments\Concerns\HasComments; // confirm exact FQCN in vendor/

class Ticket extends Model
{
    use HasComments;
}
// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
    return $panel->plugin(NestedCommentsPlugin::make());
}

First use case: a comment thread on an internal record view. The cheapest end-to-end proof is a ViewTicket page in an existing Filament resource — drop the package's infolist component into the page schema to render the thread, and its form component to post. That gets you create → reply → nested render in an afternoon, on a surface where the audience is authenticated staff and the blast radius is small.

Before you demo it, run the four spike checks: post a reply three levels deep, delete a parent, log in as a user who should not be able to comment, and count the queries on a thread with 50 replies. Those four results decide whether you keep it.

Implementation Patterns

Wrap it behind an internal interface from day one. The single highest-leverage decision. A CommentRepository (or Commentable contract) with reply(), threadFor(), delete() costs about a day and makes the package replaceable. Controllers, Filament actions, and policies talk to your interface; only the adapter knows the package exists.

interface CommentRepository
{
    public function reply(Model $commentable, User $author, string $body, ?int $parentId = null): Comment;
    public function threadFor(Model $commentable): LengthAwarePaginator;
}

Two-component split: infolist for reading, form for writing. In practice you attach the infolist component to the record's view page (read-only thread, no Livewire round-trip on every navigation) and the form component to a dedicated action or a separate page for composing. Trying to make one component do both leads to Livewire state headaches — a CreateAction modal with a parent_id hidden field is usually cleaner than an always-visible reply box.

Replies are just a create with parent_id. Model the "Reply" button as a Filament action on each comment that opens a form pre-filled with the parent's ID. Keep the depth cap in your own layer:

// app/Observers/CommentObserver.php  — your code, not the package's
public function creating(Comment $comment): void
{
    if ($comment->parent_id && $this->depthOf($comment->parent_id) >= 5) {
        throw new DepthLimitExceeded();
    }
}

The package will happily let you nest 200 levels deep. That's a render-time DoS and a UX failure, and it's yours to prevent.

Authorization belongs in policies, not in the component. Assume the package ships no meaningful policy hooks until you prove otherwise. Gate every path yourself:

// CommentPolicy
public function create(User $user, Ticket $ticket): bool
{
    return $user->can('view', $ticket);
}

public function delete(User $user, Comment $comment): bool
{
    return $user->id === $comment->user_id || $user->hasRole('admin');
}

Then verify the component actually calls authorize() — check by logging in as a user without permission and clicking. If it renders the form anyway, wrap the component in a ->visible(fn () => Gate::allows(...)) at the schema level.

Eager-load and denormalize before you scale. The default access pattern is recursive child loading, which is an N+1 factory. For anything beyond a prototype:

  • Eager load childrenRecursive (or whatever the model names it) on thread fetch.
  • Add a replies_count column and increment it in an observer — cheap, and it lets you collapse threads without counting.
  • Index (commentable_type, commentable_id), parent_id, (commentable_id, created_at).
Schema::table('comments', function (Blueprint $table) {
    $table->index(['commentable_type', 'commentable_id']);
    $table->index('parent_id');
    $table->index('created_at');
});

Roll out behind a feature flag to one panel surface, with query-count and render-time instrumentation from the first deploy. When the thread page shows 80 queries on a record with 30 comments, you want to know in staging, not from a support ticket.

Sanitize on the write path, not the render path. Strip or escape HTML when the comment is created. If you allow markdown, render through a configured CommonMark instance with html_input => 'strip'. Do not trust the package's rendering to escape — verify it by posting <script>alert(1)</script> as the first thing you do.

Gotchas and Tips

Filament major version is the blocking constraint. Not Laravel, not PHP. Filament's schema/form/infolist component APIs have moved materially between majors, and a package pinned to the previous one may be unfixable without a fork. Establish the supported matrix before any other work — it's the cheapest possible kill criterion.

Published views outside the panel's Tailwind content paths fail silently. The thread renders, but unstyled and broken-looking. Add the published view directory to your theme's content array in tailwind.config.js and rebuild. This burns hours if you don't know to look for it.

Parent deletion behavior is unspecified until you test it. Delete a comment with three children and see what happens: orphaned children (they vanish from the thread because the parent no longer renders), cascade-deleted children (data loss), or tombstoned. Pick a behavior and enforce it in an observer. On a support-ticket surface, tombstoning ("[deleted]") with children preserved is almost always the right answer — hard-deleting a thread mid-conversation destroys context for everyone below.

Morph key type mismatch is a silent install-killer. If your app uses UUID/ULID primary keys, confirm the package's commentable_id migration uses uuidMorphs (or a string column), not morphs (bigint). A mismatch doesn't error on install — it fails on first insert with a truncated value or a type error. Check the migration before running it.

Check tenancy scoping explicitly. Filament's multi-tenancy applies global scopes to plugin-supplied queries inconsistently. If your panel is multi-tenant, log in as tenant A and confirm you cannot see tenant B's comments on any record. Cross-tenant leakage here is severity-one and the package will not have tested for it.

Published views drift from vendor views with no warning. Keep a short docs/comments-overrides.md listing which views you've overridden and why. Otherwise the next engineer "fixes" a bug that was already fixed upstream, or reverts your sanitization.

Pin an exact version and vendor the code if it survives the spike. "coolsam/nested-comments": "1.0.0", not ^1.0. Add it to your Filament upgrade checklist so a panel major bump triggers a validation pass rather than a QA surprise. Set a pre-agreed fork trigger — e.g. "no release within 90 days of a Filament major" — and assign a named owner before it reaches production.

Livewire state after posting needs an explicit refresh. After a successful create, the thread component may not re-render the new reply without a $this->dispatch('$refresh') or a key change on the component. Test the post-then-see-it flow, not just the post.

Debugging tip: when a component doesn't render at all, php artisan filament:optimize-clear and check the plugin registration order in the panel provider. When it renders but looks wrong, it's almost always the Tailwind content path. When it renders correctly but is slow, it's the N+1 — count queries, don't guess.

Test against your actual DB driver. SQLite in CI and MySQL in production diverge on index length limits and recursive query support. If the package uses any recursive CTE internally, SQLite test runs will behave differently from production.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json