Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Datatables Bundle Laravel Package

tejadong/datatables-bundle

A Laravel/PHP bundle for integrating DataTables with server-side processing. Build sortable, searchable, paginated tables from Eloquent or query builder data via a simple API, with flexible configuration for columns, filters, and responses.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check before you start: despite the generic name, this is a Symfony bundle (Sg\DatatablesBundle lineage, forked by tejadong), not a Laravel package. There is no service provider, no facade, no Blade directive, no config/datatables.php to publish. If you're on Laravel, do not install this — use yajra/laravel-datatables. The only reason to reach for this fork is if you maintain a Symfony app (2.x–5.x era, depending on the fork's composer.json) and need DataTables.js server-side processing over Doctrine entities. You can, however, read it as a reference implementation for how server-side DataTables should behave: it models the request/response contract (draw, start, length, order[], search, columns[]) very cleanly.

Minimal path on Symfony:

  1. Install and pin. composer require tejadong/datatables-bundle. With 0 stars and 0 dependents, pin an exact version in composer.json and commit the lockfile. Check the fork's composer.json constraints against your Symfony and Doctrine versions before anything else — this is where most forks die. If any column path traverses a Doctrine collection (see below), you need >=2.0.4; earlier 2.0.x releases silently return one row per collection member.
  2. Register the bundle in config/bundles.php / AppKernel::registerBundles(). There is no Flex recipe; you add it by hand.
  3. Create a DataTable class under e.g. src/Datatables/PostDatatable.php extending AbstractDatatable, implement buildDatatable(), getEntity(), getName():
public function buildDatatable(array $options = [])
{
    $this->ajax->set(['url' => $this->router->generate('post_datatable_results')]);
    $this->features->set(['server_side' => true, 'processing' => true, 'state_save' => true]);
    $this->options->set(['individual_filtering' => false, 'order_cells_top' => true]);

    $this->columnBuilder
        ->add('id', Column::class, ['title' => 'ID'])
        ->add('title', Column::class, ['title' => 'Title'])
        ->add('author.name', Column::class, ['title' => 'Author'])
        ->add('createdAt', DateTimeColumn::class, ['title' => 'Created']);
}
  1. Register it as a service and inject router, request_stack, security.authorization_checker, translator (constructor signature varies by fork version — copy it from AbstractDatatable).
  2. Two controller actions: one renders the page with the datatable object, one returns $datatable->getResponse() after setQuery($qb).
  3. Twig: {{ datatable_render(datatable) }} (or _html + _js split) — the bundle emits both the <table> markup and the $(...).DataTable({...}) init script.

First use case to build: a read-only paginated listing over one entity, no joins, no actions. Verify recordsTotal/recordsFiltered come back sane before adding anything else. This package rewards incremental adoption; every feature you bolt on before the base query paginates correctly will make debugging ambiguous.

Second use case, once the first is green: one many-to-one association column (author.name). Only after that works should you attempt a path that crosses a collection (pertenencias.centroDeportivo.nombre) — that's the shape that 2.0.4 fixes, and it exercises a different code path (GROUP_CONCAT + collection joins) than a plain association.

Implementation Patterns

  • Treat the DataTable class as configuration, the controller as the query owner. The idiomatic split: column/feature/option definitions live in the DataTable subclass; the QueryBuilder is assembled in the controller (or a repository method) and injected via setQuery(). This keeps the DataTable class reusable across different filters/tenants.
  • Entity name vs. QueryBuilder. getEntity() returning 'AppBundle:Post' is the zero-effort path and only works for the simplest case. The moment you need a WHERE on a relation, a soft-delete condition, or a GROUP BY, switch to setQuery() with an explicit createQueryBuilder('post') — and make sure the root alias matches the alias used in column names.
  • Association columns use dot notation (author.name). The bundle resolves these to joins automatically in the entity path; with a custom QB you usually need to add the leftJoin yourself and keep the alias matching (post.author → author.name).
  • Collection paths are a distinct, more fragile code path. A column path like pertenencias.centroDeportivo.nombre crosses a OneToMany/ManyToMany and therefore cannot be a plain join — the bundle marks those joins as collection joins and aggregates the scalar with GROUP_CONCAT in setSelect()/GROUP BY. As of 2.0.4, every join from the first collection onward in the path is flagged (the $collectionJoins array) and none of their identifiers leak into SELECT/GROUP BY. Before that, only the final join was treated as the collection, so intermediate collection identifiers polluted the grouping. Practical rules:
    • Keep the whole path in the column name; don't hand-join part of it in the controller and let the bundle join the rest — you'll split the collection detection.
    • Expect one GROUP_CONCAT-bundled string per row for that column; it's display-only. Don't filter or sort on it.
    • If you must sort/filter by something inside a collection, use a scalar subquery or a denormalised column on the root entity instead.
  • Column types over raw strings. Column, BooleanColumn, DateTimeColumn, TextColumn, LinkColumn, ActionColumn. DateTimeColumn handles client-side formatting; ActionColumn with a Route object is how you get edit/delete buttons without hand-writing JS.
  • Rendering is client-side. For badges/avatar/links, use Column::class with 'render' => 'function(data, type, row, meta) { ... }' rather than dumping HTML from the server. Keep the server payload data-only. This applies doubly to collection columns — GROUP_CONCAT output is a delimited string, so split it in the render callback, not in Twig.
  • Individual filtering (individual_filtering => true) puts a search input in the header. It multiplies query count and is a common source of "search box appears but does nothing" tickets — pair it with setSearchColumns() on each column that should participate.
  • Actions + CSRF. Inline editing and delete buttons post back to your own routes; the bundle doesn't do CSRF for you. Put the token in the ActionColumn render payload or a data- attribute read by your JS handler.
  • Extract a base class. Every real app ends up with a BaseDatatable that pre-sets processing, server_side, state_save, pageLength, responsive/autoWidth, and the translator locale, then subclasses only declare columns. This is the single biggest maintainability win with this package.
  • Pair with a dedicated results route per table, named consistently (post_datatable_results), and check authorization there with a voter. The results endpoint is a data-leak surface: it will happily serialize whatever columns you declared, regardless of who's asking.
  • For Laravel readers / ports: the transferable pieces are AbstractDatatable::getResponse() (request parsing → draw/recordsTotal/recordsFiltered/data), the column-to-DQL mapping, and the search/order application. The collection handling in setSelect() is the piece most home-grown implementations get wrong — replicate the "flag all joins from the first collection onward" rule, not just "flag the last join". It's roughly 4–6 classes to reimplement on top of a Laravel query builder — a reasonable weekend port if you're rebuilding on Symfony-adjacent tooling.

Gotchas and Tips

  • The service is stateful and buildDatatable() gets called twice (once to render, once in the results action). Never accumulate mutable state inside it; anything set in buildDatatable must be idempotent.
  • Symfony version drift is the main failure mode. The parent bundle (stwe/DatatablesBundle) is effectively abandoned, and forks like this one chase PHP 8 / Symfony 5+ compatibility. Before debugging any bug, confirm the fork's composer.json actually supports your Symfony minor — otherwise you'll chase ArgumentCountErrors in the AbstractDatatable constructor.
  • 0 stars / 0 dependents is a supply-chain signal. The package is small enough to vendor: copy the classes into src/ and own them. That removes the abandoned-dependency risk entirely and costs you an afternoon. The 2.0.4 fix is a good example of why: the bug sat in collection handling, it's a ~15-line change in setSelect(), and you'd rather have it in your own repo than wait on a maintainer.
  • If you're on < 2.0.4 and use collection paths, here is exactly what the bug looks like. With a column such as pertenencias.centroDeportivo.nombre, only the last join was treated as a collection, so the intermediate join's identifier stayed in the SELECT and GROUP BY. Symptoms, in the order you'll notice them:
    • iTotalDisplayRecords / recordsFiltered is higher than the real row count (one per collection element, not per root entity) — so the pager says "1 of 812" for a 90-row table.
    • The GROUP_CONCAT column shows a single value instead of the comma-joined list, because each group now has one member.
    • Pagination is skewed and page boundaries overlap/skip. Diagnose with the raw count query, not the JS: if SELECT COUNT(...) on the QB with the same joins returns the inflated number, it's this bug — upgrade to 2.0.4 rather than patching the query.
  • 2.0.4 is a patch release with no API change — no constructor signatures, no column-definition changes, no config. It is a drop-in upgrade. The one thing to check: if you worked around the bug (e.g. added ->groupBy('post.id') manually, or added a DISTINCT, or used a scalar subquery instead of a collection path), remove the workaround after upgrading, because the bundle now emits its own grouping and you'll double up. Same for any having() hack on COUNT.
  • Filters on intermediate collection aliases still work. The fix only changes what's selected/grouped, not what's joined — so existing WHERE clauses against pertenencias/centroDeportivo aliases keep behaving identically. Good place to add a regression test after upgrading.
  • recordsTotal vs recordsFiltered is your smoke test. If recordsTotal is wrong, your count query is broken (usually raw SQL columns or a GROUP BY); if only recordsFiltered is wrong, your search/where clause is broken. Check both before touching the JS. Collection bugs almost always show up as recordsFiltered > recordsTotal or both inflated.
  • Raw/addColumn-style computed columns break the count query and produce pagination that shows "1 of 500" but only pages through 12 rows. If a column is computed, mark it non-searchable/non-orderable explicitly.
  • LIKE %term% on large tables is a full scan. For anything past ~100k rows, switch searchable columns to a MATCH ... AGAINST or a searchable filter in the controller, and consider disabling state_save (it triggers extra requests on every reload and confuses users debugging pagination).
  • Don't wrap the response. getResponse() returns a JsonResponse already shaped for DataTables. Wrapping or post-processing data breaks draw matching and silently freezes the table.
  • Debug in the network tab, not the PHP log. Inspect the data query string payload (start/length/order/search), then replay it with curl against the results route with your session cookie. If the JSON is correct and the table is empty, it's a columns[].data name mismatch — the most common bug in this whole ecosystem. If the JSON is wrong, log the fully-built DQL ($qb->getDQL() plus bound params) and compare the GROUP BY list against your column paths — that's how you'd have spotted the 2.0.4 bug in minutes.
  • Ordering on association columns requires the join to exist and the column to be orderable. Ordering by a hidden or computed column often yields "no sorting" with no error. Ordering by a collection-aggregated column is not supported at all — mark it orderable => false.
  • Extension points are the option objects ($this->ajax, $this->features, $this->options, $this->callbacks, $this->events) — they're thin wrappers that emit raw DataTables config. When the bundle lacks a feature, set the raw JS option through $this->callbacks/$this->events rather than patching the class.
  • There's no config file to tweak. Everything is PHP in the DataTable class. Resist the urge to add a YAML layer; it will fight the bundle's fluent API and you'll be the only one who understands it.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle