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

Nova Comments Laravel Package

kirschbaum-development/nova-comments

A Laravel Nova package that adds comment threads to your resources, letting teams discuss records without leaving the admin panel. Configure which models accept comments and keep collaboration close to the data.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a Nova field plus a polymorphic Comment model — there is no application to learn, only four edits to make. Budget half a day for the first integration, including a negative authorization test.

The four edits, in order:

composer require kirschbaum-development/nova-comments
php artisan vendor:publish --provider="Kirschbaum\NovaComments\NovaCommentsServiceProvider"
php artisan migrate

Confirm the provider namespace by listing vendor/kirschbaum-development/nova-comments/src before you copy the command — the class name is the one thing worth verifying rather than trusting. Publish by tag, not bare --force, so a later re-publish doesn't overwrite the migration you've already customized.

Then:

  1. Attach HasComments to the target model — the concrete model, not an abstract base, unless every subclass genuinely wants a thread.
  2. Register the field on the resource, detail-screen only:
use Kirschbaum\NovaComments\Fields\Comments;

public function fields(NovaRequest $request): array
{
    return [
        // ...
        Comments::make()->onlyOnDetail(),
    ];
}
  1. Authorize it before anyone sees it. Out of the box the panel is visible to anyone who can view the resource. Add canSee / canRun and the policy behind them on day one, not after launch.

Where to look first, in this order. Skip the README; read the code you just published.

  • The published migration — does it index (commentable_type, commentable_id)? Does the author FK use nullOnDelete or (badly) cascadeOnDelete?
  • The published Comment model — $fillable, casts, and how the author relation is resolved.
  • The config file — which user model and which comment model it points at. That's your extension surface.
  • The field class in vendor/ — which Nova abilities it checks on create and destroy. The docs won't tell you; this file will.

First use case to validate: an internal handoff thread on an Order or Ticket detail page — "customer called back, refund promised", "escalated to finance". It's low-stakes, internal-only, and it proves the two things that actually matter: that the panel renders on your Nova version, and that the authorization you wrote holds for a non-privileged user. Do not launch with a customer-facing or compliance-flavored use case; those are a different class of product.

Smoke test: create, edit, delete a comment as an admin, then reload the same page as a user who shouldn't see the panel. The second check is the one that matters.

Implementation Patterns

Make adding comments to the Nth resource mechanical. Once the first integration works, the day-to-day work is repetition. Write yourself a checklist: trait on the model, field in fields(), policy methods, morph map entry (see below), factory state for tests. Ten minutes per resource, and no decisions to re-litigate.

Decide the morph map on day one. This is the single decision that's expensive to reverse. If you ever call Relation::enforceMorphMap() after rows exist, old rows store FQCNs and new rows store aliases and you're writing a data migration. Choose explicitly now:

// AppServiceProvider::boot()
Relation::enforceMorphMap([
    'order'  => \App\Models\Order::class,
    'ticket' => \App\Models\Ticket::class,
]);

Or accept FQCNs forever. Don't drift into the middle.

Gate on the record, not the role. The valuable pattern is authorization that resolves against this specific record, which is what lets you say "sales can comment on their own accounts, support on any ticket, nobody on invoices":

Comments::make()
    ->onlyOnDetail()
    ->canSee(fn (NovaRequest $request) => $request->user()->can('viewComments', $this->resource))
    ->canRun(fn (NovaRequest $request) => $request->user()->can('createComment', $this->resource));

Put the ability methods in a policy on the target model (or a dedicated CommentPolicy) rather than scattering role checks across resources. When the inevitable "who can read vs. write?" question arrives, the answer is one file, not a grep.

Notifications go in an observer, not the field. The most common follow-up request is "notify the assignee when someone comments." Register a Comment::created observer that dispatches a queued notification to the relevant people except the author. Keeping it out of the Nova layer means the same behavior fires from console commands, imports, and seeders.

Comment::created(function (Comment $comment) {
    $comment->commentable
        ->notifiableUsers()
        ->reject(fn ($u) => $u->is($comment->author))
        ->each(fn ($u) => $u->notify(new CommentPosted($comment)));
});

Own the model, own the schema. The strongest argument for this package over a hand-rolled field is that persistence is plain Eloquent. Publish the migration and treat comments as your table: add deleted_at, edited_at, a visibility enum, an author_name snapshot, or team_id. Extend the package Comment, point the config at your subclass, and you keep the field UI while growing a data model that survives leaving Nova entirely.

// config/nova-comments.php
'comment_model' => \App\Models\Comment::class,

Query comments outside Nova. The same relation powers digests, exports, and reporting — never build a second notes table for those.

$order->comments()->with('author')->latest()->get();

// Weekly handoff digest
Comment::where('commentable_type', (new Order)->getMorphClass())
    ->where('created_at', '>=', now()->subWeek())
    ->get()
    ->groupBy('commentable_id');

Note getMorphClass() rather than Order::class — it respects the morph map whether or not you adopted one. Write one export command on day one; it kills the lock-in objection before anyone raises it.

Unread badges are a schema decision, not a package feature. Add a read_at per-user join table (comments are per-record, readers are per-team — a single column won't model it), and a whereDoesntHave('reads', ...) scope the field can use or the resource can surface as a tab badge.

Test the denial path, not the happy path. Use RefreshDatabase, factory a model with the trait, actingAs a Nova user:

public function test_user_without_ability_cannot_see_comments(): void
{
    $this->actingAs($this->userWithoutPermission())
        ->getJson("/nova-api/orders/{$this->order->id}")
        ->assertJsonMissing(['component' => 'comments']);
}

The field rendering successfully proves very little. The negative case is your actual coverage.

Gotchas and Tips

  • Nova 5 support landed in a patch release (4.0.3), not a minor. A routine composer update changed Nova-version code paths without a semver signal. Read the composer.json constraints to establish whether Nova 4 is retained or the floor was raised, then pin deliberately. If you're on Nova 4 for several more quarters, upgrading is optional and non-zero risk — deferring is defensible.
  • Silent failure is the default failure mode. A Nova upgrade breaks the field's Vue components and the panel just renders empty. No error, no log, nobody files a ticket — you find out from a user, weeks later. Put "comments panel" on the Nova upgrade checklist with a manual smoke test on one resource.
  • Asset build is part of the upgrade. The dependency churn in recent releases touches the Node toolchain. Verify npm run build / nova:publish compiles under your Node version in staging before shipping to production.
  • Check the polymorphic index. The migration should index (commentable_type, commentable_id). A missing composite index on morph lookups is a slow query that only shows up in production. Add it yourself if it isn't there.
  • Orphaned comments are permanent. Polymorphic relations can't cascade at the DB level. Delete comments in the target model's deleting event, or accept orphans knowingly — and decide the soft-delete case deliberately rather than inheriting an accident.
  • Author attribution when users leave. A hard FK with cascadeOnDelete silently erases history. Use nullOnDelete and keep an author_name snapshot so a thread still reads "Jane Doe (deactivated)" three years later.
  • Defaults are permissive. Assume anyone with resource access can comment and delete until you've read the field's ability checks in vendor source. Audit the code, then write the denial test.
  • Multi-tenancy is not free. The polymorphic relation scopes to the model instance, not a tenant. Add a global scope or a policy check, otherwise a leaked commentable ID becomes a cross-tenant read.
  • Sanitize on output. A comment thread is user-generated content rendered inside your admin panel. If you allow anything past plain text, escape at render; don't rely on the field to do it.
  • vendor:publish --force overwrites your migration and config. Once either is customized, publish by tag and diff first. Commit the published migration rather than leaning on loadMigrationsFrom.
  • config:cache after config edits. Publishing config in a cached environment and wondering why comment_model changes don't apply is a reliable twenty-minute detour.
  • Don't let it become the audit log. Teams reliably start using comments as a record of "who changed what, when." Name that boundary in your own repo's README before a compliance question arrives with a different answer than the one you assumed.
  • Extension points worth knowing now: swap the comment model via config, add columns to the published migration, register observers for notifications, and override canSee/canRun on the field. Those four cover most real requests. Anything beyond — threading, attachments, reactions, email-in reply — is a fork you own, so push back on the ticket rather than patching vendor code.
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.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings