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

Laravel Comments Laravel Package

ryangjchandler/laravel-comments

Add threaded comments to any Laravel Eloquent model with a simple trait. laravel-comments provides polymorphic comment storage, nested replies, and a clean API for creating, querying, and managing comments in your app.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish.

composer require ryangjchandler/laravel-comments
php artisan vendor:publish --tag=comments-config
php artisan vendor:publish --tag=comments-migrations
php artisan migrate

The service provider is auto-discovered. Publish before migrating — running migrate first and publishing afterwards is the most common first-install mistake, and you'll then get a "file already exists" prompt with nothing useful created.

Read the migration before you run it. This is your one chance to shape the schema cheaply. Check the author foreign key (nullable? users table? UUID/ULID?) and decide now whether you need parent_id for threading, a status column for moderation, or edited_at for edit history. Adding them in the published migration is fine; adding them later means a second migration and a config-aware model subclass.

Wire up your models.

use RyanChandler\Comments\Concerns\HasComments;

class Post extends Model
{
    use HasComments;
}

class User extends Authenticatable
{
    use RyanChandler\Comments\Concerns\Commenter;
}

HasComments gives the polymorphic relation on any model; Commenter gives the inverse (a user's comments) plus the convenience helpers. Because the relation is polymorphic, one install serves posts, videos, tickets, products — you just add the trait per model.

First use case: comment on a post, render the thread.

// In a controller, with an authenticated user in context
$post->comment($request->validated('text'));

// Explicit / non-request contexts
$post->comments()->create([
    'user_id' => $user->id,
    'text'    => $request->validated('text'),
]);
@foreach ($comments as $comment)
    <div>{{ $comment->user?->name ?? 'Deleted user' }}: {{ $comment->text }}</div>
@endforeach

Where to look when something is off, in this order: the published config/comments.php, the published migration (column names and FK), then the package's Comment model and Concerns/ directory in vendor/. Almost every "it isn't working" report traces to one of those three rather than to package logic. The package is small enough to read end-to-end in an afternoon — do that once and your debugging speed doubles.

Implementation Patterns

Keep HTTP concerns in your app. The package is a persistence layer; controllers, validation and authorization belong to you. A FormRequest plus a policy gives you rate limiting, "comments closed" checks and spam hooks without touching vendor code:

public function store(StoreCommentRequest $request, Post $post)
{
    $this->authorize('comment', $post);

    $post->comment($request->validated('text'));

    return back();
}

Render threads without N+1. The author relation is the hot path — always eager load, always paginate at the top level:

$post->comments()
    ->with('user')
    ->latest()
    ->paginate(20);

Then use $comment->user?->name ?? 'Deleted user' in Blade. If you make the author FK nullable, every view needs that null-safe access.

Customise via the config, never by editing vendor. Point the config's comment model key at your own subclass (App\Models\Comment extends RyanChandler\Comments\Models\Comment), and put approval status, soft deletes, a body accessor or a isApproved() helper there. Every relation and trait helper resolves through the configured class, so the swap is transparent. If a model swap "doesn't take effect", it's almost always a cached config — php artisan config:clear, or config:cache after the change in production.

Use model events, not package hooks. Comments are real Eloquent models, so standard observers are the integration point for notifications, feeds and moderation queues:

// AppServiceProvider::boot()
Comment::created(fn ($comment) => CommentCreated::dispatch($comment));

This keeps the fan-out (mail, database notifications, Slack) out of the request cycle and out of the package's scope.

Extend the schema in your own migration. Treat the published migration as a starting point rather than a contract. New columns (status, edited_at, pinned, reaction counters) go in a follow-up migration so future package upgrades never clobber them.

Threading. Replies are just comments with a parent reference. Decide early on max depth (the package exposes configurable nesting) and decide whether you allow unlimited depth in the UI or cap rendering at two levels with a "view thread" link — deeply nested Blade recursion is a performance trap.

API surfaces. Return an ApiResource rather than serialising the model directly: it hides commentable_type / commentable_id, normalises the author shape, and lets you add computed fields (reply count, can-edit) without touching the model.

Front-end stacks. For Livewire, Inertia or Filament, treat the package as persistence only. Bind a component or prop to $post->comments()->with('user')->latest()->paginate(), post to your own controller, and re-render from the relation. Avoid calling trait helpers directly from a Filament action or queued job without passing the user explicitly — see the auth caveat below.

Testing. Use RefreshDatabase and assert against the database, not package internals:

$this->actingAs($user)->post("/posts/{$post->id}/comments", ['text' => 'Hi']);

$this->assertDatabaseHas('comments', [
    'commentable_id'   => $post->id,
    'commentable_type' => Post::class,
    'user_id'          => $user->id,
]);

Write your own CommentFactory if the package doesn't ship one for your version — you'll use it constantly for thread-rendering tests.

Gotchas and Tips

Publish before migratе, every time. Covered above, but worth repeating because the failure is silent: migrate creates nothing, then vendor:publish warns and skips or overwrites. On a fresh clone, always run publish commands first.

Morph map stability. commentable_type stores the FQCN unless you call Relation::enforceMorphMap(). If you introduce a morph map after comments exist, historical rows point at a class string your map no longer recognises and the threads come back empty. Decide on aliases (or explicitly on FQCNs) at install time, not in sprint three.

No cascade deletes. Polymorphic relations can't carry a database foreign key, so deleting a Post leaves orphaned comments rows. Handle it explicitly in the model or a cleanup job:

static::deleting(fn ($post) => $post->comments()->delete());

For bulk deletions, prefer a queued job so a large post purge doesn't lock the comments table in a request.

Auth resolution outside HTTP. auth()->user() is null in queued jobs, console commands, seeders and some Livewire test contexts. Depending on your schema that either creates an author-less comment or throws on a non-nullable user_id. Always pass the user explicitly in non-request code: $post->comments()->create(['user_id' => $user->id, 'text' => ...]).

Debug empty threads with SQL, not with the UI. Start with Comment::latest()->get() and $post->comments()->toSql(). Nine times out of ten the cause is a commentable_type mismatch (morph map, or the trait added to a different model class than the one being queried) rather than a package bug.

Config caching bites on model swaps. After changing the configured comment model, run php artisan config:cache in production and config:clear locally. A cached config is the usual reason a subclass appears ignored.

Don't edit vendor or the published migration post-deploy. New columns go in a new migration; behaviour goes in your subclass. This makes the next release a no-op upgrade instead of a merge exercise.

Pin sensibly. The changelog for v1.1.0 is dependency maintenance only — no behavioural detail shipped with it. In applications, commit composer.lock. In internal packages or libraries, pin ^1.0 rather than ^1.1 so downstream apps aren't forced onto a dependency matrix you haven't tested. Before merging a bump:

composer update ryangjchandler/laravel-comments -W
composer why-not laravel/framework ^10.0   # sanity-check the resolved matrix
php artisan test

Budget the work the package doesn't do. It supplies storage, threading and the relation plumbing; you still own authorization policies, sanitisation of rendered content (never render raw user text as HTML without a sanitiser), rate limiting, spam/abuse checks, notification fan-out, edit windows, soft deletes and audit trails. Scoping those as part of the feature — not as a follow-up — is what separates a two-day integration from a two-month one.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums