Search by

happenv-com / filament-comments

webard

Comments module for Filament

v2.1.0 2026-09-25 09:31 UTC

README

Filament Comments

Latest Version Tests PHPStan Quality Total Downloads License

Polymorphic, paginated comments for Filament 4 and 5. Drop a Comments component into any infolist or form schema and the current record gets a comment list with a rich-text (or Markdown) form, quoting, shareable deep links and pagination.

use Happenv\FilamentComments\Filament\Components\Comments;

$schema->components([
    Comments::make(), // uses the record's `comments` relationship
]);

Key features

  • Comments on any record. Add the HasComments trait to a model and a Comments component to its schema — see Usage.
  • Rich text or Markdown. Comments are written in Filament's rich editor, with optional mentions, or in the Markdown editor — see Comment format.
  • Quoting. One click quotes a comment into the form, as a blockquote in rich text or as > lines in Markdown.
  • Shareable deep links. Every comment has a "copy link" action; opening the link jumps to the page containing the comment and highlights it — see Deep links.
  • Three pagination types. Simple, standard (with page numbers) or cursor pagination, with a per-page selector above, below or on both sides of the list — see Pagination.
  • Several lists per record. Each relationship (e.g. comments and internalNotes) gets its own independent list on the same page.
  • Safe by default. The list's configuration is validated once and stored in a locked Livewire property, so the browser cannot change it — see Extending.
  • Replaceable parts. The save action, the form, the comment item and every part of it are classes you can swap — see Customizing.

Requirements

Package Versions
PHP 8.3 – 8.5
Laravel 12, 13
Filament 4 (^4.5.0), 5 (^5.0)
Livewire 3 (Filament 4), 4 (Filament 5)

Installation

Install the package via Composer:

composer require happenv-com/filament-comments

Publish and run the migration that creates the comments table:

php artisan vendor:publish --tag=happenv-filament-comments-migrations
php artisan migrate

The table has an auto-incrementing id, a polymorphic commentable relation created with nullableMorphs() (so commentable_id is an integer column), an author_id foreign key to the users table, a content text column and timestamps. Edit the published migration before running it if your models use UUID or ULID keys (see UUID or ULID keys) or your authors are not stored in users.

Important

If you have not set up a custom theme and are using Filament Panels, follow the instructions in the Filament docs (4.x, 5.x) first.

Add the package's views to your theme's CSS file, so Tailwind generates the classes they use:

@source '../../../../vendor/happenv-com/filament-comments/resources/**/*.blade.php';

Configuration

Optionally publish the config file:

php artisan vendor:publish --tag=happenv-filament-comments-config

This is the content of the published config file, config/filament-comments.php:

return [
    /*
     * Model of comment authors. Defaults to the model of the "users" auth provider.
     */
    'author_model' => null,

    /*
     * Model of comments. Must extend Happenv\FilamentComments\Models\Comment,
     * e.g. to add HasUuids together with a matching migration.
     */
    'comment_model' => null,
];
Key Default What it does
author_model null Eloquent model of comment authors (Comment::author(), the default mention provider). null falls back to auth.providers.users.model.
comment_model null Eloquent model of comments, used by HasComments. Must extend Happenv\FilamentComments\Models\Comment. null uses the package's own model.

The author model does not depend on the logged-in user. If neither setting points to an Eloquent model, a LogicException is thrown. For example:

'author_model' => App\Models\Admin::class,

Optionally, publish the views and translations:

php artisan vendor:publish --tag=happenv-filament-comments-views
php artisan vendor:publish --tag=happenv-filament-comments-translations

UUID or ULID keys

The migration uses standard auto-incrementing keys. If your models use UUIDs (or ULIDs), change the published migration before running it:

  • Commentable models with UUID keys — replace nullableMorphs('commentable') with nullableUuidMorphs('commentable') (or nullableUlidMorphs()).
  • UUID keys for the comments themselves — replace $table->id() with $table->uuid('id')->primary(), and point comment_model to your own model that adds HasUuids:
namespace App\Models;

use Happenv\FilamentComments\Models\Comment as BaseComment;
use Illuminate\Database\Eloquent\Concerns\HasUuids;

class Comment extends BaseComment
{
    use HasUuids;
}
// config/filament-comments.php
'comment_model' => App\Models\Comment::class,

Relationships you define yourself (such as internalNotes() below) should use the same model.

Usage

Make a model commentable

use Happenv\FilamentComments\Concerns\HasComments;

class Issue extends Model
{
    use HasComments;
}

HasComments adds a comments() morph-many relationship. You can define more relationships on the same model (for example internalNotes()); every relationship must declare its return type (MorphMany, HasMany, ...):

use Happenv\FilamentComments\Models\Comment;
use Illuminate\Database\Eloquent\Relations\MorphMany;

public function internalNotes(): MorphMany
{
    return $this->morphMany(Comment::class, 'commentable')
        ->where('content', 'like', '%[internal]%');
}

Add the component to a schema

use Happenv\FilamentComments\Filament\Components\Comments;

public static function infolist(Schema $schema): Schema
{
    return $schema->components([
        Comments::make(), // uses the `comments` relationship
    ]);
}

Comments::make() takes the name of the relationship, comments by default. Two components on the same page (e.g. Comments::make('comments') and Comments::make('internalNotes')) render two independent lists.

The component is hidden while the schema has no record (e.g. on a create page). It is a regular schema component, so the usual methods such as visible() and hidden() work too.

Component options

Every setting accepts a value or a closure returning it. Closures are evaluated once, when the list is mounted, with the usual Filament injections ($record, $livewire, ...).

Method Default What it does
formLocation() CommentFormLocation::Above Where the comment form is shown: Above or Below the list.
paginationType() CommentsPaginationType::Simple Simple, Standard or Cursor pagination.
paginationLocation() CommentsPaginationLocation::Below Where the pagination is shown: Above, Below, Both or None.
paginationOptions() [10, 20, 50] Page sizes the user can pick.
paginationDefaultPerPage() 20 Initial page size; must be one of the options.
commentFormat() CommentFormat::Html Html (rich editor) or Markdown (Markdown editor).
sortColumn() 'created_at' Column the list is sorted by, newest first.
commentItemContentFieldName() 'content' Attribute of the comment model that holds its content.
canComment() true Whether the form and the quote action are shown and comments can be submitted.
mentionProvider() / mentionProviders() none Mention providers of the rich editor.
saveAction() SaveCommentAction::class Class that saves a new comment.
formSchema() CommentFormSchema::class Class that builds the comment form.
itemSchema() CommentItemSchema::class Class that builds the schema of one comment.
commentItemComponent() Comment::class Schema component that renders one comment.

All of them together:

use Happenv\FilamentComments\Enums\CommentFormat;
use Happenv\FilamentComments\Enums\CommentFormLocation;
use Happenv\FilamentComments\Enums\CommentsPaginationLocation;
use Happenv\FilamentComments\Enums\CommentsPaginationType;
use Happenv\FilamentComments\Filament\MentionProviders\UserMentionProvider;

Comments::make('internalNotes')
    ->formLocation(CommentFormLocation::Below)              // Above (default) or Below
    ->paginationType(CommentsPaginationType::Standard)      // Simple (default), Standard or Cursor
    ->paginationLocation(CommentsPaginationLocation::Both)  // Above, Below (default), Both or None
    ->paginationOptions([10, 25, 50])                       // page sizes the user can pick
    ->paginationDefaultPerPage(25)                          // must be one of the options
    ->commentFormat(CommentFormat::Markdown)                // Html (default) or Markdown
    ->sortColumn('created_at')                              // newest first, ties broken by the primary key
    ->commentItemContentFieldName('content')                // attribute that holds the comment
    ->mentionProviders([UserMentionProvider::class])
    ->canComment(fn (Issue $record): bool => auth()->user()->can('comment', $record));

Form location

Comments::make()->formLocation(CommentFormLocation::Below);

With the form above the list (the default), the newest comment is at the top. With the form below the list, each page is shown oldest to newest, so the conversation reads towards the form.

Pagination

Comments::make()
    ->paginationType(CommentsPaginationType::Cursor)
    ->paginationLocation(CommentsPaginationLocation::Above)
    ->paginationOptions([5, 10, 25])
    ->paginationDefaultPerPage(10);
  • paginationType(): Simple (previous / next, the default), Standard (page numbers) or Cursor.
  • paginationLocation(): Above, Below (the default), Both or None. The pagination is not rendered while the list is empty.
  • paginationOptions(): the page sizes in the per-page select, [10, 20, 50] by default. At least one, all positive integers.
  • paginationDefaultPerPage(): the initial page size, 20 by default. It must be one of the options. A page size sent by the browser that is not one of the options falls back to this default.

The page is kept in the query string as {relationship}_page (e.g. comments_page), so two lists on one page paginate independently. Changing the page size, or adding a comment, goes back to the first page.

Sorting

Comments::make()->sortColumn('updated_at');

Comments are sorted by this column, newest first (created_at by default), and then by primary key, so pages are stable when timestamps are equal. The column name must be a plain identifier (letters, digits and underscores).

Comment format

Comments::make()->commentFormat(CommentFormat::Markdown);
  • CommentFormat::Html (default): the form uses Filament's rich editor (bold, italic, strike, link, bullet and ordered lists, code block and blockquote, plus mentions). Comments are rendered as HTML.
  • CommentFormat::Markdown: the form uses Filament's Markdown editor, and comments are rendered with the Markdown parser.

Content attribute

Comments::make()->commentItemContentFieldName('body');

The attribute of the comment model that the form writes to and the list renders, content by default. It must be a plain identifier.

Who can comment

Comments::make()->canComment(fn (Issue $record): bool => auth()->user()->can('comment', $record));

true by default. When it is false, the form and the quote action are hidden and the list refuses to save a comment. Submitting a comment also requires a logged-in user and non-empty content; otherwise the user gets a notification and nothing is saved.

Mentions

use Happenv\FilamentComments\Filament\MentionProviders\UserMentionProvider;

Comments::make()->mentionProvider(UserMentionProvider::class);

// or several at once; both methods add to the list
Comments::make()->mentionProviders([UserMentionProvider::class, TeamMentionProvider::class]);

Mention providers are only used by the rich editor (CommentFormat::Html). None are registered by default. A provider is a class implementing Contracts\ProvidesMentions — a static make() method returning a Filament MentionProvider:

use Filament\Forms\Components\RichEditor\MentionProvider;
use Happenv\FilamentComments\Contracts\ProvidesMentions;

class TeamMentionProvider implements ProvidesMentions
{
    public static function make(): MentionProvider
    {
        return MentionProvider::make('#')
            ->getSearchResultsUsing(fn (string $search): array => Team::query()
                ->where('name', 'like', "%{$search}%")
                ->pluck('name', 'id')
                ->all())
            ->getLabelsUsing(fn (array $ids): array => Team::query()
                ->whereIn('id', $ids)
                ->pluck('name', 'id')
                ->all());
    }
}

The bundled UserMentionProvider searches the name column of the author model (up to 10 results) with the @ character.

Deep links

Every comment has a "copy link" action that copies a link to the current page with the comment in the query string, e.g. ?comments_comment_id=42 (the parameter is {relationship}_comment_id). Opening the link opens the page containing the comment and highlights it. This works with all three pagination types.

Rendering comment content

The comment model registers its content attribute as Filament rich content. By default the attribute gets the UserMentionProvider mentions. A commentable model can configure the attribute itself with a setUpCommentsRichContent() method (called on a new instance of the commentable model), which returns a closure that receives the attribute. The default mentions are then not added:

use Closure;
use Filament\Forms\Components\RichEditor\RichContentAttribute;
use Happenv\FilamentComments\Models\Comment;

class Issue extends Model
{
    use HasComments;

    public function setUpCommentsRichContent(Comment $comment): Closure
    {
        return fn (RichContentAttribute $attribute): RichContentAttribute => $attribute
            ->mentions([TeamMentionProvider::make()]);
    }
}

Events

The list dispatches two browser events, which you can listen to with Alpine or Livewire:

Event Parameters When
comment-added relationship After a comment is saved.
highlight-comment commentId, relationship When a deep link opens; the list scrolls to the comment and highlights it.
<div x-on:comment-added.window="if ($event.detail.relationship === 'comments') $wire.$refresh()">
    ...
</div>

Validation

Invalid configuration throws an InvalidArgumentException when the list is built:

  • the default page size must be one of paginationOptions(), and the options must be positive integers;
  • the relationship name, sort column and content field must be plain identifiers;
  • extension classes must implement their contracts (see Customizing);
  • the relationship method must exist and declare a relation return type (e.g. : MorphMany).

Extending

The list is rendered by a nested Livewire component (Livewire\CommentsList, registered as happenv-filament-comments-list). The component receives its configuration as one validated, immutable CommentsSettings object, stored in a locked Livewire property, so the browser can't change it. Build the settings from a component with Comments::make('comments')->...->toSettings(). For the same reason, extension points are class names that implement a contract, not closures.

Customizing

Setting Contract Default
saveAction() Contracts\SavesComment Actions\SaveCommentAction
formSchema() Contracts\ConfiguresCommentForm Filament\Schemas\CommentFormSchema
itemSchema() Contracts\ConfiguresCommentItem Filament\Schemas\CommentItemSchema
commentItemComponent() extends Filament\Components\Comment Filament\Components\Comment
mentionProviders() Contracts\ProvidesMentions none

Each of them accepts a class name, or a closure that returns one.

Save action

The save action is invoked with the record, the validated form state, the logged-in author and the settings, and returns the saved comment. A custom save action, for example:

use Happenv\FilamentComments\Actions\SaveCommentAction;
use Happenv\FilamentComments\Contracts\SavesComment;
use Happenv\FilamentComments\Support\CommentsSettings;

class SaveAndNotify implements SavesComment
{
    public function __invoke(Model $record, array $data, Authenticatable $author, CommentsSettings $settings): Model
    {
        $comment = app(SaveCommentAction::class)($record, $data, $author, $settings);

        $record->owner->notify(new NewComment($comment));

        return $comment;
    }
}

Comments::make()->saveAction(SaveAndNotify::class);

Form schema

The form schema class builds the comment form. Extend CommentFormSchema and override htmlFormatInput() or markdownFormatInput() to change the editor, or implement ConfiguresCommentForm from scratch. The form must call the list's submitComment method:

use Filament\Forms\Components\RichEditor;
use Happenv\FilamentComments\Filament\Schemas\CommentFormSchema;

class CompactCommentForm extends CommentFormSchema
{
    public static function htmlFormatInput(string $contentField, array $mentions): RichEditor
    {
        return parent::htmlFormatInput($contentField, $mentions)
            ->toolbarButtons(['bold', 'italic', 'link']);
    }
}

Comments::make()->formSchema(CompactCommentForm::class);

Item schema

The item schema class builds the schema of one comment. The default one renders commentItemComponent() for the content field, in Markdown mode for CommentFormat::Markdown:

use Filament\Schemas\Schema;
use Happenv\FilamentComments\Contracts\ConfiguresCommentItem;
use Happenv\FilamentComments\Support\CommentsSettings;

class CompactCommentItem implements ConfiguresCommentItem
{
    public static function configure(Schema $schema, CommentsSettings $settings): Schema
    {
        return $schema->components([
            $settings->itemComponent::make($settings->contentField)
                ->footerComponent(false),
        ]);
    }
}

Comments::make()->itemSchema(CompactCommentItem::class);

Comment item component

Filament\Components\Comment renders one comment: a header (author and date), the content and a footer (quote and share actions). Every part accepts another component, a closure returning one, or false to remove it — headerComponent(), authorComponent(), createdAtComponent(), contentComponent() and footerComponent() — and markdown() switches the content to the Markdown parser. The parts are resolved lazily, so configuration applied after make() takes effect.

To change the defaults for every comment, extend the component and override any of the getDefault*Component() methods (header, author, created at, content, footer) or the getQuoteAction() / getShareAction() methods:

use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Component;
use Happenv\FilamentComments\Filament\Components\Comment;

class CommentWithEmail extends Comment
{
    public function getDefaultAuthorComponent(): Component | false | null
    {
        return TextEntry::make('author.email')->hiddenLabel();
    }
}

Comments::make()->commentItemComponent(CommentWithEmail::class);

Comment model

HasComments resolves the comment model from the container, so you can replace it with a subclass of Models\Comment in a service provider:

use Happenv\FilamentComments\Models\Comment;

public function register(): void
{
    $this->app->bind(Comment::class, App\Models\Comment::class);
}

Testing your application

The comment model has a factory. Its withAuthor() state sets the author; without it, the factory creates an author with the author model's own factory:

use Happenv\FilamentComments\Models\Comment;

$comment = Comment::factory()
    ->for($issue, 'commentable')
    ->withAuthor($user)
    ->create(['content' => '<p>Looks good</p>']);

Translations

The package ships in every locale Filament ships:

am ar az bg bn bs ca ckb cs da de el en es et eu fa fi fil fr he hi hr hu hy id it ja ka km ko ku lt lus lv mk mn ms my nb ne nl pl pt pt_BR ro ru sk sl sq sr_Cyrl sr_Latn sv sw tg th tr uk ur uz vi zh_CN zh_HK zh_TW

Publish them to change the texts:

php artisan vendor:publish --tag=happenv-filament-comments-translations

tests/Unit/TranslationsTest.php checks that every language has exactly the keys English has, that nothing is left in English, and that every locale Filament ships has a translation.

Development

composer test          # unit and feature tests
composer phpstan       # static analysis
composer cs            # fix code style: composer normalize, Rector, Pint
composer ci            # everything CI checks, locally

The test suite runs on Pest against Filament 4 (Livewire 3) and Filament 5 (Livewire 4), on Laravel 12 and 13.

Upgrading

Breaking changes and how to migrate are described in UPGRADING for every major version.

Changelog

See CHANGELOG and GitHub releases for what has changed recently.

Contributing

See CONTRIBUTING for details.

Security vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). See License File for more information.

Happenv