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 Laravel Package

pragmarx/datatables

pragmarx/datatables offers a fluent Laravel integration for server-side DataTables, letting you build sortable, filterable, paginated tables from Eloquent queries or arrays with concise PHP code. It handles request parameters, column search, ordering, and JSON responses for dynamic admin grids.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements: Laravel 4.x, PHP 5.4+. This package does not support Laravel 5+ — if you are on a modern app, stop here and use yajra/laravel-datatables instead.

Install:

composer require pragmarx/datatables:dev-master

Register it in app/config/app.php:

'providers' => [
    // ...
    'PragmaRX\Datatables\DatatablesServiceProvider',
],

'aliases' => [
    // ...
    'Datatables' => 'PragmaRX\Datatables\Facade\Datatables',
],

Optionally publish the config (goes to app/config/packages/pragmarx/datatables/):

php artisan config:publish pragmarx/datatables

First use case — a paginated, searchable, sortable users table, server-side:

// app/routes.php
Route::get('datatables/users', function () {
    return Datatables::of(User::select('id', 'name', 'email'))
        ->make();
});
$('#users').dataTable({
    bProcessing: true,
    bServerSide: true,
    sAjaxSource: '/datatables/users',
    aoColumns: [
        { mDataProp: 'id'    },
        { mDataProp: 'name'  },
        { mDataProp: 'email' }
    ]
});

Where to look first: the README is ~a page. Then read the service provider and the request/serializer class (the thing that parses iDisplayStart, sSearch, iSortCol_0) — the whole package is a few hundred lines. The config file tells you which defaults (page size, wrapper key, etc.) are overridable.

The package has a very small surface area: you feed it a query builder, it reads DataTables' request params, and it returns the JSON envelope (sEcho, iTotalRecords, iTotalDisplayRecords, aaData). Day-to-day, you will spend most of your time on the query and the column mapping, not on the package.

Implementation Patterns

Thin route, fat query. Keep the route as a one-liner and put real work in the query:

Route::get('datatables/orders', function () {
    $q = Order::with('customer')
        ->select('orders.id', 'orders.total', 'customers.name')
        ->join('customers', 'customers.id', '=', 'orders.customer_id');

    return Datatables::of($q)->make();
});

One route per table, named consistently (datatables/users, datatables/orders). Move them into a DatatablesController once you have more than two — closures make it hard to test and to reuse filters.

Whitelist the columns you expose. Never let DataTables' index-based sort params (iSortCol_0) drive ORDER BY unchecked. Map the index to a validated column name:

$sortable = ['0' => 'orders.id', '1' => 'orders.total', '2' => 'customers.name'];

Pre-filter before handing off. Anything you do with where(...) before Datatables::of() narrows both the "total" and the "filtered" record set, which is usually what a tenant-scoped or archived-scoped table wants:

$q = Order::where('tenant_id', Auth::user()->tenant_id)->select(...);

Custom display values. If the package exposes a per-column callback (check the config/serializer), use it; otherwise alias in the select (DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d') as created")) or add an accessor on the model. Do not post-process aaData in JavaScript if you need the search to match — filtering happens in SQL.

Multiple tables on one page is fine as long as each sAjaxSource is a distinct route; avoid sharing a route with a ?type= param, it makes the column mapping branchy and buggy.

Testing. In L4 you can $this->call('GET', 'datatables/users', ['iDisplayStart' => 0, 'iDisplayLength' => 10, 'sEcho' => 1]) and assert on json_decode($response->getContent()). Assert iTotalRecords, iTotalDisplayRecords and the shape of aaData — those are the three things that break.

Gotchas and Tips

  • DataTables 1.9 vs 1.10 protocol. This package predates DataTables 1.10 and speaks the legacy parameter names (iDisplayStart, iDisplayLength, sEcho, sSearch, iSortCol_0, sSortDir_0). If the client uses 1.10's { processing, serverSide, ajax } style, either set $.fn.dataTable.ext.legacy.ajax = true client-side or you will get empty tables / broken paging. This is the #1 source of "it returns data but the table is blank."

  • sEcho must be echoed verbatim. DataTables compares the returned sEcho to the value it sent. If the package drops it (or you short-circuit the response), you get the "DataTables warning: table id=… Ajax error" and/or out-of-order responses.

  • Total vs filtered. iTotalRecords is the unfiltered count; iTotalDisplayRecords is the count after search filters. Swapping them produces a pager that says "1 of 400" while showing 3 rows, or worse, an infinite "next" button. Verify both with a search term applied.

  • Column index drift. DataTables sends indices including hidden/non-orderable columns. If your select() drops or reorders a column relative to the JS aoColumns array, sorting silently targets the wrong column. Keep the JS column list and the SQL select list in the same order, and keep an explicit whitelist map.

  • Sort direction injection. iSortDir_0 is user-controlled. Coerce it: strtolower($dir) === 'desc' ? 'desc' : 'asc'.

  • Leading-wildcard LIKE. The built-in global search does LIKE '%term%', which cannot use an index. On tables with >100k rows it will time out before the package's serialization ever runs. Scope it with a pre-filter or move to a dedicated search column / full-text index.

  • Duplicate column names on joins. select * with a join yields id twice; the row hydrates the last one and you get confusing mismatches. Always select('orders.id as id', ...) with aliases.

  • Debugging. Bypass the browser: curl 'http://app/datatables/users?iDisplayStart=0&iDisplayLength=10&sEcho=1&iSortCol_0=0&sSortDir_0=asc' and inspect the raw JSON. Also Log::info(Input::all()) at the top of the route to see exactly what the client sent — half the bugs are "client sent 1.10 params, server expected 1.9."

  • Config location. php artisan config:publish writes to app/config/packages/pragmarx/datatables/, not app/config/. Editing the wrong file is a classic 20-minute waste. L4 has no config:cache, so no cache-busting needed.

  • Extension points are thin. The package is small; if you need behavior it doesn't offer (custom response envelope, relationship-aware search, per-column formatters), subclass the handler/serializer and override, or just write the response yourself — the JSON contract is five keys and DataTables does not care who builds it.

  • It's abandoned. 16 stars, 0 dependents, no commits since the Laravel 4 era. If you must keep an L4 app alive, fork and pin ("pragmarx/datatables": "dev-master#<sha>") so a Composer update can't pull a broken master. For any new work, port the endpoint to yajra/laravel-datatables — the route shape and JSON contract are the same, so the front-end table needs no changes.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin