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

Datatable Bundle Laravel Package

crossknowledge/datatable-bundle

A Symfony bundle for building datatables with server-side data handling, reusable configuration, and straightforward integration into PHP admin interfaces. It helps teams add sortable, filterable tables to back-office screens without duplicating table logic.

View on GitHub
Deep Wiki
Context7

Getting Started

Before you composer require anything, understand what you're adopting: crossknowledge/datatable-bundle is a Symfony bundle, not a Laravel package. There is no ServiceProvider, no facade, no config publish tag, and its data source layer is built on Doctrine ORM QueryBuilder — not Eloquent. The DataTableFactory you see in the docs is resolved out of Symfony's DI container. In a pure Laravel app you cannot pull it in and call it a day; you'd be bootstrapping a Symfony kernel and a Doctrine EntityManager inside Laravel, which is almost never worth it.

So treat this package as one of three things, in order of realistic value:

  1. A protocol reference — the authoritative implementation of the DataTables server-side contract (draw, start, length, order, search → paginated JSON). Highest value for a Laravel dev.
  2. A sidecar service — run it in a real Symfony app (monorepo or separate "reporting" service) and call its JSON endpoint from Laravel.
  3. A direct dependency — only if your stack already includes Symfony + Doctrine.

Where to look first (regardless of path):

  • DataTableFactory — how a table is constructed and how handleRequest() parses the payload.
  • DataSourceInterface (fetchData() / fetchCount()) — the abstraction worth copying.
  • Column\Formatter\FormatterInterface — per-column value formatting.
  • The JSON response shape: draw, recordsTotal, recordsFiltered, data.

Recommended first use case: pick your heaviest admin index (users, orders, audit logs) and reimplement the bundle's protocol handling on Eloquent. One table, end to end, is enough to validate the pattern.

If you do run it for real, the install is:

composer require crossknowledge/datatable-bundle
# requires symfony/framework-bundle, doctrine/orm, twig/twig ^3.0
// config/bundles.php — Symfony app only
CrossKnowledge\DataTableBundle\CrossKnowledgeDataTableBundle::class => ['all' => true],

Frontend is unchanged either way:

$('#example').DataTable({
  processing: true,
  serverSide: true,
  ajax: '/your-endpoint'
});

Implementation Patterns

1. Copy the protocol contract into Laravel (the 80% case)

DataTables always sends the same shape, so the work is deterministic: parse, whitelist, query, count, map, respond.

// app/DataTables/ServerSideTable.php
abstract class ServerSideTable
{
    abstract protected function query(): Builder;
    abstract protected function columns(): array;   // ['name' => ['label' => ..., 'sortable' => true, 'searchable' => true, 'format' => fn($v) => ...]]

    public function handle(Request $request): JsonResponse
    {
        $draw   = (int) $request->input('draw', 1);
        $start  = (int) $request->input('start', 0);
        $length = (int) $request->input('length', 25);

        $base = $this->query();
        $total = (clone $base)->count();

        // global search across whitelisted searchable columns
        $term = $request->input('search.value');
        if ($term) {
            $base->where(function ($q) use ($term) {
                foreach ($this->columns() as $col => $meta) {
                    if ($meta['searchable'] ?? false) {
                        $q->orWhere($col, 'like', "%{$term}%");
                    }
                }
            });
        }
        $filtered = (clone $base)->count();

        // order — never trust the client's column name blindly
        $orderIdx = (int) $request->input('order.0.column', 0);
        $orderDir = $request->input('order.0.dir') === 'desc' ? 'desc' : 'asc';
        $orderCol = array_keys($this->columns())[$orderIdx] ?? 'id';
        $base->orderBy($orderCol, $orderDir);

        $rows = $base->skip($start)->take($length)->get();

        return response()->json([
            'draw'            => $draw,
            'recordsTotal'    => $total,
            'recordsFiltered' => $filtered,
            'data'            => $rows->map(fn ($r) => $this->mapRow($r))->all(),
        ]);
    }
}

2. Mirror the DataSourceInterface abstraction

The bundle's best idea is separating "where rows come from" from "how they're presented" (fetchData() / fetchCount()). Port it so Elasticsearch, an API, or an array source plugs in the same way:

interface DataSourceInterface
{
    public function fetchData(array $options): array;   // start, length, order, search
    public function fetchCount(array $options): int;
}

class EloquentDataSource implements DataSourceInterface { /* ... */ }
class ElasticDataSource  implements DataSourceInterface { /* ... */ }

This is also your escape hatch if you later move the table into a Symfony service — the shape is already compatible.

3. Column definition objects, not controller spaghetti

Mirror addColumn() / setFormatter() with a fluent column builder so formatters live next to their column:

$this->columns = [
    'id'         => Column::make('ID'),
    'name'       => Column::make('Name')->searchable()->sortable(),
    'created_at' => Column::make('Created')
        ->format(fn ($v) => $v->format('Y-m-d')),
];

4. If you must consume the actual bundle: JSON-only, sidecar

Run it in a Symfony service, keep the endpoint pure JSON (no Twig rendering), and bridge auth (shared session/JWT). This is also insurance: the 4.1.0 "upgrade twig" release becomes a no-op for you because you never render through Twig.

$dataTable = $factory->create();
$dataTable->setDataSource($queryBuilder);   // Doctrine QueryBuilder
$dataTable->addColumn('id', 'ID');
return $dataTable->handleRequest($request);

Call it from Laravel with Http::withToken(...)->get('/reporting/users', $clientParams) and pass the DataTables params straight through.

5. Testing

Don't mock the whole HTTP stack. Build a fake payload and assert the JSON shape (draw echoed back, recordsTotal ≥ recordsFiltered, correct page size):

$response = $this->getJson('/admin/users?draw=2&start=10&length=10');
$response->assertJson(['draw' => 2])->assertJsonCount(10, 'data');

Gotchas and Tips

  • It is not a Laravel package. No auto-discovery, no php artisan vendor:publish, no Eloquent adapter. setDataSource() expects Doctrine QueryBuilder. Any "just install it" plan in a Laravel codebase is a port project in disguise. The native Laravel equivalent is yajra/laravel-datatables — reach for that if Laravel is the only stack; use this bundle only when Symfony/Doctrine is already in play.
  • Never interpolate client-supplied column names into SQL. DataTables posts column indices, and some client configs post names. Always map index → whitelisted column as in the pattern above. This is the single most common injection vector in hand-rolled DataTables endpoints.
  • Counts drift from filters. recordsFiltered must be computed with exactly the same WHERE clause as the data query. Joins + DISTINCT/GROUP BY break naive count() — in Doctrine this is a classic pitfall, and it translates directly to Eloquent: use distinct()->count('id') or a subquery count, and test with a filtered query, not just the unfiltered one.
  • N+1 inside formatters. A formatter closure runs per row. If it touches $row->customer->name, you've just fired length extra queries. Eager-load what formatters need, or select the joined column directly.
  • Select only what you need. Fetching full entities/models for a 100-row page with 40 relations is how a "fast" server-side table ends up slower than client-side. Constrain the base query before pagination.
  • draw must be echoed back verbatim (as an integer). If you return a string or drop it, DataTables silently discards the response and you get a spinner forever — a genuinely confusing symptom.
  • Twig 3 fallout (4.1.0) only affects templating. If you're JSON-only, ignore it. If you have custom Symfony Twig extensions, rename Twig_Extension → Twig\Extension\AbstractExtension, Twig_SimpleFunction → Twig\TwigFunction. Pin twig/twig: ^3.0 in root composer.json; check blockers with composer why-not twig/twig ^3.0.
  • Debugging the payload: dump the raw request first (dump($request->query->all()) in Symfony; dd($request->all()) in Laravel). Nine out of ten "it returns nothing" bugs are a mismatched param name (search[value] vs search) or a missing columns[i][data] mapping.
  • Log the SQL. Symfony: bundle debug: true. Laravel: DB::listen(fn ($q) => logger($q->sql)) — you'll immediately see whether the count query picked up the search WHERE.
  • Config quirks: default_length, default_order, and locale are set in cross_knowledge_data_table.yaml. Remember default_order there is only a fallback — the client's order param wins, so your "sensible default" can be silently overridden by saved DataTables state.
  • Extension points worth stealing even after a port: the DataSourceInterface (custom/non-Doctrine sources), the FormatterInterface (reusable currency/date formatters instead of inline closures), and the data_table.build.query event (mutate the query without touching the table class — in Laravel, a QueryBuilder macro or a TableBuilding event does the same job).
  • Low bus factor signal: 4 stars and 0 dependents. Budget for reading the source rather than the docs, and keep your ported Laravel implementation dependency-free so an abandoned upstream doesn't strand your admin panel.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource