bllim/datatables
Laravel package that adds server-side support for the DataTables jQuery plugin, letting you build AJAX-powered tables with pagination, searching, and sorting through a simple fluent API with minimal setup.
bllim/datatables is a Laravel 4-era server-side processor for the DataTables jQuery plugin. It takes a query builder or Eloquent query, reads DataTables' draw/start/length/search/order request parameters, and returns the JSON envelope (draw, recordsTotal, recordsFiltered, data) that the client expects. As of v1.5.0-alpha it also claims Laravel 5 support — treat that claim as unverified (see Gotchas).
Minimal setup on Laravel 4:
composer require bllim/datatables:~1.3.app/config/app.php: 'Bllim\Datatables\DatatablesServiceProvider'.'Datatables' => 'Bllim\Datatables\Datatables'.php artisan config:publish bllim/datatables (this is a Laravel 4 command; on L5 use php artisan vendor:publish instead).Minimal setup on Laravel 5 (alpha, expect friction):
composer require bllim/datatables:1.5.0-alpha — a pre-release tag, so you'll need either an exact constraint like this one or "minimum-stability": "dev" + "prefer-stable": true in composer.json. A bare ~1.5 will not resolve.'Bllim\Datatables\DatatablesServiceProvider' in config/app.php providers and the Datatables alias in aliases (no auto-discovery — the package predates it).php artisan vendor:publish to get the config; verify where it lands (the alpha may still target app/config/packages/... rather than config/).php artisan tinker then app('datatables'). If that fails, stop — the rest of the integration will not work.Where to look first: the package's README (it contains the canonical of()/addColumn() examples) and the published config file, which controls things like the request parameter names and default search behavior. The main entry point is the Datatables facade — almost everything is a fluent chain ending in ->make(). There is no changelog beyond the release note and no test suite documented, so read the diff between 1.3 and 1.5.0-alpha before trusting it in a real app.
First use case — a users table with an ajax endpoint:
// routes.php / routes/web.php
Route::get('users/data', function () {
return Datatables::of(User::select(['id', 'name', 'email']))
->addColumn('action', 'partials.user-actions') // Blade view
->make();
});
$('#users').DataTable({
processing: true,
serverSide: true,
ajax: '/users/data',
columns: [
{ data: 'id', name: 'id' },
{ data: 'name', name: 'name' },
{ data: 'email', name: 'email' },
{ data: 'action', name: 'action', orderable: false, searchable: false }
]
});
If the table renders rows but pagination or search does nothing, the problem is almost always a mismatch between columns in JS and the selected columns in the query — start there.
Basic of() with Eloquent. Prefer select([...]) over select('*') so joins and eager loads stay predictable. The package handles skip/take and the global LIKE search for you based on the column definitions, so keep the query honest (don't pre-paginate it).
Column presentation. Use editColumn('email', function ($row) { ... }) to transform an existing column in place, and addColumn('label', function ($row) { ... }) to append a column that isn't in the query. For anything with markup (buttons, badges, links), point at a Blade view instead of a closure string:
->addColumn('action', 'partials.user-actions') // view receives $row
This keeps HTML out of PHP and lets you reuse the same partial across tables. This pattern is version-agnostic and is the part of the API most likely to survive the L5 alpha unchanged.
Custom filtering. Anything the user does via a filter widget (date range, status dropdown, tenant scope) goes through filter():
Datatables::of($query)
->filter(function ($query) {
if (request()->has('status')) {
$query->where('status', request('status'));
}
if (request()->has('from')) {
$query->where('created_at', '>=', request('from'));
}
});
Prefer the request() helper / injected Illuminate\Http\Request over the Input facade if you're on L5 — Input is still aliased by default but is deprecated and easy to strip out of config/app.php during a cleanup, which would silently break these closures. On L4, Input::has() / Input::get() are the idiomatic calls.
This runs only when a search/filter is present and keeps your base query reusable for other endpoints.
Filtering and sorting computed columns. Columns created with addColumn don't exist in the database, so DataTables' search and order will error or silently no-op. Register them explicitly:
->filterColumn('full_name', function ($query, $keyword) {
$query->whereRaw("CONCAT(first_name, ' ', last_name) like ?", ["%{$keyword}%"]);
})
->orderColumn('full_name', function ($query, $direction) {
$query->orderByRaw("CONCAT(first_name, ' ', last_name) {$direction}");
})
$direction is interpolated directly into raw SQL here, so normalize it ($direction === 'desc' ? 'desc' : 'asc') if you don't trust the client — DataTables sends it, but so can anyone else.
Row-level metadata. setRowClass, setRowId, and setRowData map to DataTables' DT_RowClass, DT_RowId, and DT_RowData, which is how you attach IDs and state to rows for click handlers and bulk actions.
Relations. Eager-load with with('company') and flatten in editColumn('company.name', ...), or expose the nested key directly (data: 'company.name' in JS). Don't lazy-load inside a column closure — that's an N+1 query per row.
Non-DataTables consumption. The same chain works for exports and internal APIs: call skipPaging() and setTotalRecords() when you need all rows, or use the package purely as a filtering/ordering parser by consuming the built query without calling make().
Layering. A common pattern is a single DatatableController (or per-resource controllers) whose data() method owns the query composition, while the Blade view owns the table markup. Keep the filter closures in the controller, not the view, so they're testable and reusable from a second endpoint (e.g. CSV export using the same query). On L5, register the endpoint in routes/web.php (not api.php) if the table relies on session/CSRF.
Version pinning as a pattern. Because 1.5.0-alpha is a moving target, either pin the exact alpha tag or keep the L4 ~1.3 line and don't mix. Don't write ^1.5 against a pre-release you haven't verified — the next alpha can change behaviour with no changelog.
illuminate/* constraints may conflict, and the facade/alias registration plus vendor:publish paths are the first things to break.composer require bllim/datatables:^1.5 fails or silently picks 1.3; you need 1.5.0-alpha, ~1.5.0@alpha, or minimum-stability: dev. Symptom: "Could not find package bllim/datatables at version 1.5" even though the tag exists.yajra/laravel-datatables, the direct successor with a near-identical fluent API. Migrating later is cheaper than debugging this alpha now.app/config/app.php; on L5 it's config/app.php — the class names are the same but the file path and publish command differ.config:publish vs vendor:publish. L4's php artisan config:publish bllim/datatables does not exist on L5. Conversely vendor:publish on the alpha may publish into a path the package doesn't actually read. Check that your edits to the published config have an effect (change a parameter name and watch the request fail) before relying on it.of() / addColumn() / editColumn / make() may have been touched to get L5 running. Wrap the chain in one controller method per resource so a version bump only breaks one place.order[0][column] is a zero-based index into your JS columns array. If you add a hidden column or an action column, sort indexes shift and sorting starts applying to the wrong column. Recheck the JS columns array first whenever sorting seems "off by one."select() must include the ordering column and (for Eloquent) the primary key. Selecting only display columns breaks orderBy and some relation resolution.select('*') produce duplicate id columns; always qualify (users.id) and explicitly list columns when joining.escapeColumns([]) or a specific column list. Do this deliberately — turning escaping off globally for a table containing user-supplied data is an XSS vector.addColumn columns is a no-op unless you add filterColumn/orderColumn, as above. Symptom: typing in the search box filters correctly for real columns but never matches the computed ones.type: 'POST', the token handling differs between L4 and L5 (_token field vs X-CSRF-TOKEN header). A 500 or silent empty table on POST but not GET is usually this — and on L5 it will bite you if the route lands in api.php without the web middleware group.recordsTotal should reflect the unfiltered count and recordsFiltered the post-search count. If both are equal to the page size, you've likely pre-applied take()/skip() to the query. If draw is missing or mismatched, the client will stall.composer dump-autoload, re-check config/app.php) from "the package loads but returns bad JSON" (query composition). On the alpha, most reports are the former.null. Returning null from a closure can render as "null" in older DataTables versions; return '' for intentionally blank cells.ajax.data shape or use a DataTables fork with renamed parameters, update the config rather than patching the package.addColumn/editColumn, the cleanest extension surface is Blade partials for cell rendering and filter/filterColumn closures for query behavior. Subclassing Bllim\Datatables\Datatables to override response assembly is possible but brittle — the internals were not designed as a stable API, and the alpha increases that risk rather than reducing it.How can I help you explore Laravel packages today?