Mercure Integration
Real-time DataTables refresh with Mercure SSE
Use Mercure when table data can change outside the current browser session and you want automatic refresh.
MercureConfig can be configured manually through DataTable::mercure(...),
declared on #[AsDataTable(...)], or auto-resolved from
#[AsDataTable(..., mercure: true)].
What The Integration Does
When mercure is configured on a table:
- Backend options include a
mercureobject (hubUrl,topics, optionalwithCredentials, optionaldebounceMs). - The Stimulus controller opens an
EventSourceto the hub with?topic=.... - On each message, the controller emits a
datatables:mercure:messageevent and triggerstable.ajax.reload(null, false). - The SSE connection is closed automatically on controller
disconnect().
Minimal Setup
#[AsDataTable(Book::class, mercure: true)]
final class BookDataTable extends AbstractDataTable
{
}
When auto-resolution is enabled:
- the bundle reads the default Symfony Mercure hub URL
- explicit API Platform
mercure.topicsare reused when available - otherwise the bundle falls back to an item IRI template (for example
/api/books/{id}) - if no API Platform item metadata is available, it falls back to
/datatables/books/{id}
Attribute Topics
Use explicit topics on the attribute when you do not want topic auto-resolution from API Platform metadata:
#[AsDataTable(
entityClass: Book::class,
mercure: [
'topics' => [
'https://example.com/books',
],
],
)]
final class BookDataTable extends AbstractDataTable
{
}
The same form accepts one topic or several topics:
#[AsDataTable(
entityClass: Book::class,
mercure: [
'topics' => [
'https://example.com/books',
'https://example.com/authors',
],
'withCredentials' => true,
'debounceMs' => 300,
],
)]
Mercure supports multiple subscriptions by repeating the topic query
parameter. For most tables, one topic is enough. Use several topics when a
single DataTable must refresh after changes published on different resources or
channels.
Advanced Setup
$dataTable->mercure(
topics: ['admin/books', 'admin/authors'],
withCredentials: true,
debounceMs: 300,
);
Notes:
withCredentialsenables cookie/auth forwarding in the SSE request.debounceMsis applied client-side before reload (500msby default).topicsaccepts one or many topics.
Publishing Updates
If symfony/mercure is available, the bundle registers MercureUpdatePublisher and injects it into the Ajax edit controller.
When boolean inline edit is called with topics, the controller publishes:
{
"type": "edit",
"id": "...",
"field": "..."
}
You can also publish manually with MercureUpdatePublisher::publish() or publishForDataTable().
Highlighting Updated Cells
A refresh that changes a value silently is easy to miss. highlightUpdates() briefly emphasizes the
cells a Mercure refresh changed:
public function configureDataTable(DataTable $table): DataTable
{
return $table
->mercure(topics: ['https://example.com/users'])
->highlightUpdates(ignoreColumns: ['lastLoginAt'])
->serverSide();
}
| Parameter | Default | Purpose |
|---|---|---|
durationMs | 1200 | How long a cell stays emphasized. |
ignoreColumns | [] | Data keys never highlighted. |
idField | 'id' | Property read to identify a row. |
ignoreColumns matters more than it looks. A column whose rendered value changes on every refresh -
a relative date, a counter, an “updated at” stamp - would be highlighted on every single event and
bury the real updates under constant noise. List those columns here.
How It Works
- Enabling the option serializes a
highlightobject into the frontend payload and adds aDT_RowIdkey to every row, which also feeds the DataTablesrowIdoption. API Platform rows never go through the PHP row mapper, so there the key is added client-side fromidField. - On an incoming Mercure message the controller records the values currently displayed, then reloads the table.
- Once that reload has redrawn the table, rows are matched by
DT_RowIdand compared field by field. Changed cells receive thedt-cell-updatedclass, and adatatables:highlightevent carries their nodes. A sort, a search or a paging draw landing while the refresh is in flight leaves the recorded values untouched.
Comparison runs on row data rather than on rendered markup: a redraw regenerates cell HTML even where nothing changed, so comparing markup would light up the whole table. Matching rows by id means a row that only moved to another position is not reported as updated.
Only refreshes triggered by Mercure are compared - sorting, searching, and paging never highlight.
Styling
The animation lives in the bundle stylesheet and is driven by two custom properties:
:root {
--dt-highlight-color: rgb(16 185 129 / 0.3);
}
--dt-highlight-duration is written inline from durationMs. Under
prefers-reduced-motion: reduce the animation is replaced by a static background.
To drive your own effect instead, skip the class and listen to the event:
document.addEventListener('datatables:highlight', (event) => {
for (const cell of event.detail.cells) {
// ...
}
})
Operational Notes
- Mercure refresh uses
ajax.reload(...): tables configured only with staticdatawill not auto-refresh from SSE. - No Mercure config means no SSE subscription (dynamic import is skipped).
- If the table receives many events, tune
debounceMsto protect backend load. private: truefrom API Platform metadata is not mapped automatically towithCredentials.highlightUpdates()needs rows to expose an identifier; a table whoseidFieldcannot be read leaves those rows unhighlighted.