Comment metadata
Every comment has a nullable meta json column — jsonb on PostgreSQL, json on MySQL, text on SQLite — cast to an array. It holds what your app needs to know about a comment that the body and status don’t say: the channel it came in through, the role of its author, an internal-note flag. The column ships in its own migration since 1.1.0 — see Installation for the upgrade step.
Writing metadata
Attach it with meta() on the builder, or with the meta: argument of WriteCommentData:
use RoundlyConsulting\Comments\DataTransferObjects\WriteCommentData;
use RoundlyConsulting\Comments\Facades\Comments;
Comments::on($ticket)
->as($agent)
->body('Checked the logs, looks like a timeout.')
->visible(false)
->meta(['tickets' => ['internal' => true, 'role' => 'agent', 'channel' => 'web']])
->post();
Comments::write(new WriteCommentData(commentable: $post, body: 'Hi', meta: ['source' => 'import']));- Repeated meta() calls merge: a later call’s top-level keys replace earlier ones and the other keys are kept — a shallow array_replace, not a deep merge.
- Namespace your keys (['tickets' => [...]]) so two packages, or a package and your app, writing to the same comment never overwrite each other.
- Empty means none: meta([]), the default, stores nothing and the column stays null.
- Key order is not kept — PostgreSQL jsonb and MySQL reorder object keys, so compare content, not order.
- Comments::update() leaves metadata untouched, and there is no method to change it afterwards — write it when you post.
Comments::on($ticket)
->body('Forwarded from the help desk.')
->meta(['tickets' => ['role' => 'agent']])
->meta(['source' => 'email']) // merged: 'tickets' is kept
->meta(['tickets' => ['internal' => true]]) // replaces the whole 'tickets' key
->post();
// stored: ['tickets' => ['internal' => true], 'source' => 'email']Reading metadata
| Method | Returns | Does |
|---|---|---|
$comment->meta() | array | The whole metadata array — [] when the comment has none. |
$comment->meta($key, $default = null) | mixed | One value by dot path (meta('tickets.role')), $default when the path is absent. |
$comment->hasMeta($key) | bool | Whether the path exists — a key holding null counts as present. |
$comment->meta | array|null | The raw cast attribute. |
$comment->meta(); // the whole array — [] when there is none
$comment->meta('tickets.role'); // 'agent'
$comment->meta('tickets.priority', 'normal'); // the default when the path is absent
$comment->hasMeta('tickets.internal'); // true — a key holding null counts as present
$comment->meta; // the raw cast attribute: array|nullFiltering by metadata
The fluent query filters on it, and the filters compose with everything else on CommentQuery — ordering, counts and bulk moderation:
use RoundlyConsulting\Comments\Facades\Comments;
Comments::for($ticket)->whereMeta('tickets.internal', false)->oldest()->get(); // the public replies
Comments::for($ticket)->whereMeta('tickets.role', 'agent')->count();
Comments::for($post)->whereMetaMissing('tickets')->get(); // comments no package tagged
Comments::for($ticket)->whereMeta('tickets.internal', true)->hideAll(); // composes with bulk moderation- whereMeta($path, $value) matches comments holding $value at $path. Compare with the PHP type you stored: a json boolean matches true or false, a json number an int, a json string a string — SQLite does not equate 5 with '5'. The value is a string, int or bool; for “absent”, use whereMetaMissing().
- whereMetaMissing($path) matches comments that lack $path, including comments with no metadata at all. A key holding json null is present — the same rule as hasMeta().
- Paths are lowercase dot notation, checked before they reach SQL: segments of lowercase letters, digits and underscores, none of them digits only. Anything else — Tickets.role, tickets->role, $.tickets, tickets.*, tickets[0] — throws InvalidCommentMetaPathException. The value is always a bound parameter.
- The filters use the query builder’s json path syntax, so they behave the same on SQLite, PostgreSQL and MySQL — the package’s own suite runs them on all three.
In tests and JSON
The Comment factory takes a meta([...]) state — see Testing. CommentResource leaves meta out, because metadata often holds internal flags; extend the resource to expose what your API should show.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.