---
title: "Comment metadata — Comments for Laravel | Roundly"
description: "Attach your own namespaced metadata to each comment, read it back by dot path and filter comments by it — the same on SQLite, PostgreSQL and MySQL."
url: https://roundly-consulting.com/open-source/docs/comments-for-laravel/metadata
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Comments for Laravel](https://roundly-consulting.com/open-source/docs/comments-for-laravel.md)

# 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:

```php
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.

```php
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. |

```php
$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|null
```

## Filtering by metadata

The fluent query filters on it, and the filters compose with everything else on CommentQuery — ordering, counts and bulk moderation:

```php
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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/comments-for-laravel.md)
