pentiminax/ux-datatables
Symfony UX DataTables integration that adds interactive, AJAX-powered tables to Twig with server-side processing, configurable columns, and a simple PHP API for Symfony applications.
UX DataTables is a Symfony UX bundle, not a Laravel package — there is no service provider, no config/datatables.php, no Blade directives. As a Laravel developer you'll get the most value by either (a) running it in a Symfony sidecar/admin app that shares your database, or (b) reading it as a reference implementation for the patterns (server-side column formatting, scoped bulk actions, display-aware exports) and porting them onto a Laravel stack (Livewire/Inertia + Doctrine-free query builders).
Minimal steps if you are in a Symfony app:
composer require pentiminax/ux-datatables and register the bundle; require symfony/serializer (Ajax payload mapping) and, only if you need server-side CSV/XLSX, openspout/openspout.AbstractDataTable, declare columns (TextColumn, BooleanColumn, ChoiceColumn, MoneyColumn), and implement the query source.render_datatable() from Twig (a custom id is now honored), or hit the Ajax endpoint directly.Look first at:
demo/ — a runnable Symfony app covering columns, filters, layouts, bulk actions, exports, value formatting, row action events, and theme switching. This is the fastest way to see expected output before you write a line of code.UPGRADE.md#v11--v12 — export output, money submissions, locale defaults, boolean filters, selection-column ordering offsets, modal confirmations, private Mercure.First real use case: a read-only server-side listing over a Doctrine repository with a search box, pagination, and one BooleanColumn toggle. Get that rendering and sorting correctly before touching bulk actions or exports.
Format once, render and export everywhere. AbstractColumn::formatValueUsing() receives the normalized value plus the source row, so a status enum becomes a badge label, a raw amount becomes $1,234.50, and the same formatted string flows into CSV/XLSX. On client-side tables the formatted value is what gets sorted and searched; on server-side tables the query still targets the underlying field — that asymmetry is intentional and worth designing around.
Exports are a separate formatting pipeline. Server-side exports use translated headings, translated choice labels, translated Yes/No, and money in currency units, and they preserve formatValueUsing() output. When you specifically want the raw stored values, opt out per button:
Button::csv(serverSide: true)->rawValues();
Column value formatters still apply even under rawValues() — that's the escape hatch for DB-level fidelity, not for skipping your own logic.
Row actions as an event contract. The built-in delete, detail, edit, and boolean-switch actions now dispatch datatables:action:success and datatables:action:error, with accessible failure announcements. This is the cleanest integration point for toasts, optimistic UI, or a global error banner:
document.addEventListener('datatables:action:error', (e) => notify(e.detail));
Delete/detail/edit/Ajax confirmations route through the table's modal adapter, so registering a modal library gives you consistent dialogs app-wide; native confirm() is the fallback. Link actions still use native confirmation by design.
Bulk handlers should report skips, not swallow them. When a business rule declines a row, call BulkActionContext::skip() so the summary reads "3 processed, 2 skipped" instead of silently miscounting. Note it is reporting only — it does not roll back changes already applied to that row, so validate before you mutate.
Trust the framework for scope, verify it for custom providers. For explicit Doctrine selections, identifiers outside customizeQueryBuilder() scope (including forged cross-tenant IDs) are now filtered out and counted as skipped before handlers run. Custom providers get this only by implementing ScopedIdentifierProviderInterface; API Platform tables and any provider without it must still enforce scope inside the handler. If you're porting the idea to Laravel, this maps to a whereIn constrained by a policy-scoped query — never by the posted ID list.
Private Mercure for credentialed tables. Tables with withCredentials publish mutation updates privately; subscriber JWTs must authorize the topic. Custom publishers implement PrivateUpdatePublisherInterface — existing publishers keep their interface but emit a warning when used on a private table, so grep your logs after upgrading.
Doctrine pagination for to-many joins. Distinct root entities are now paged properly for supported queries, avoiding short overlapping pages, with SQL-bounded identifier pagination where ordering allows. Grouped queries, composite identifiers, and unusual shapes keep the old path — if you have a GROUP BY grid, benchmark before assuming you got the fix.
Multi-column ordering is fully applied, plus the identifier as a tiebreaker on ordered pages. That tiebreaker is what makes page N+1 stable; don't remove it.
openspout/openspout. Server-side CSV and XLSX both need it; the feature is documented as optional, so a missing package surfaces as an export error rather than a boot failure. Add it in the same PR as your first export button.1.2.0. Stale assets produce confusing "event never fires" bugs.orderBy after the upgrade. Same class of bug as adding a column in raw DataTables.10^decimals. Money columns now edit in units consistently with the cents-backed display. If you have custom form handling that assumed cents, you will silently write values 100× off.TernaryFilter now compares true/false automatically. Use nullable() if you deliberately want NULL-based semantics; otherwise existing filtered views change behaviour.ChoiceColumn translates enum labels implementing TranslatableInterface. Tests that asserted English strings will break under a non-English locale.skip() is a counter, not a transaction. It does not undo partial work on that row — do your validation up front or wrap the handler in a transaction.ColumnResolver::removeDeniedColumnValues() (use deniedColumnPaths() + removePaths()), deep imports of filterFeature.js, filterLayout.js, bulkActionsFeature.js, bulkActionsLayout.js (the controller wires both bars), and confirmBulkAction() (use confirmAction()). Deep imports are the sneaky one — they'll keep working until 2.0, then vanish.formatValueUsing()) but are still ordering/searching the underlying field — that's expected, not a bug. The demo app has a side-by-side example.ScopedIdentifierProviderInterface (bulk selection scope for custom providers) and PrivateUpdatePublisherInterface (private Mercure publishing). Both are small interfaces and cheaper than subclassing internals.How can I help you explore Laravel packages today?