sigmasoft/datatable-bundle
Sigmasoft Datatable Bundle adds configurable server-side data tables to PHP applications, with sorting, filtering, pagination, and search support. Define columns and data sources in a few lines to render reusable, responsive tables.
Before installing anything, confirm what the package actually is. The name says Bundle (Symfony terminology) but the description claims a Laravel package. Check composer.json on the GitHub repo for laravel/framework / illuminate/* constraints and PHP version first — if it only declares Symfony/Doctrine classes, stop, because you are looking at the wrong framework. The rest of this guide assumes the Laravel claim holds.
Minimal install:
composer require sigmasoft/datatable-bundle
Laravel auto-discovery should register the service provider. If not (zero stars means the metadata may be incomplete), add it to config/app.php providers and try to publish config:
php artisan vendor:publish --tag=datatable-config
If no tag exists, there is nothing to publish — configure via the fluent API only.
Where to look first. There is no meaningful documentation beyond the README, so read src/ in this order:
draw/recordsTotal/recordsFiltered/data) or a custom envelope.Your first use case. A read-only users list view for an internal admin page: server-side pagination, sortable id/email/created_at, and a global search over name/email. This is exactly the shape the package advertises and the simplest thing to validate against.
Route + controller:
// routes/web.php
Route::get('/admin/users', [UserController::class, 'index']);
Route::get('/admin/users/datatable', [UserController::class, 'datatable'])->name('users.datatable');
// app/Http/Controllers/UserController.php
public function index()
{
return view('admin.users.index');
}
public function datatable(UserDataTable $table)
{
return $table->query(User::query())->toJson();
}
Blade: include the client-side assets yourself (jQuery + DataTables.net). The package ships the server side, not the JS/CSS. Point the DataTables ajax URL at route('users.datatable') and declare columns to match the keys your DataTable returns.
If the fluent API is DataTable::make(User::query())->column('id')->column('email', 'Email')..., wire your first table that way and verify the JSON shape in the browser network tab before moving on.
One DataTable class per resource. Resist the temptation to inline DataTable::make(...) in every controller — you will repeat column config across list views, and the class becomes the single place to add a filter or change a formatter. Follow the Laravel convention of a dedicated class per list:
class UserDataTable extends DataTable
{
public function query()
{
return User::query()->with('roles') // eager load to kill N+1
->select(['users.id', 'users.name', 'users.email', 'users.created_at']);
}
public function columns(): array
{
return [
Column::make('id')->sortable()->searchable(),
Column::make('name')->sortable()->searchable(),
Column::make('email')->sortable()->searchable(),
Column::make('roles.name', 'Roles')->formatter(fn ($v) => implode(', ', (array) $v)),
Column::make('created_at')->sortable()->formatter(fn ($v) => Carbon::parse($v)->diffForHumans()),
];
}
}
Keep formatters server-side only where possible. Client-side rendering via JS render: callbacks is fine for display, but if the formatter output is also used in CSV export, doing it in PHP (the package's fluent formatter hook) avoids drift between the table and the export.
Controller endpoint pattern. Always split the HTML page from the JSON endpoint. The page route returns the Blade shell; a separate GET returns JSON. This keeps ajax retries cheap and makes authorization per-endpoint explicit.
Authorization belongs on the query, not on the DataTable. The package will happily return whatever rows your base query returns. Apply tenant scoping and policy checks in query():
public function query()
{
$this->authorize('viewAny', User::class);
return User::query()->where('tenant_id', auth()->user()->tenant_id);
}
For per-column visibility, filter the columns() array by ability — some builders expose can() on Column, some don't. If not, hide it at the DataTable class level.
Filters as explicit methods, not pass-through. If the package supports custom filters, register them with named, whitelisted callbacks:
$this->filter('status', fn ($q, $value) => $q->where('status', $value));
Only expose a fixed set of filter keys. Never forward request('filter') wholesale into the query builder — that is the SQL injection / tenant-leak footgun.
Global search. If the package supports searchable() on columns, use it. If not, wire the search term into query() via a single where clause over a fixed column list. Never interpolate the search term into SQL.
Workflow for a new admin screen:
query() method.Integration tip: feature-flag it. Since the package (by design) touches every list view, and it is unproven, gate it behind a config flag (config('datatable.engine')) so you can flip back to hand-rolled or Yajra without a code change on every controller. Wrap the fluent builder in your own thin AdminList helper — every controller depends on your helper, not on sigmasoft/* classes directly. This makes the "replace the package" path a one-file change.
Verify it's actually Laravel before you commit. The name datatable-bundle strongly suggests a Symfony bundle (Doctrine-based, Twig-based). The description says Laravel; the two assessments in the source materials disagree. Check the namespace (Sigmasoft\... vs Illuminate\...), composer.json, and whether the service provider extends Illuminate\Support\ServiceProvider. If it's Symfony, this package cannot be used in a Laravel app without a bridge, and the time cost of building one dwarfs any benefit over Yajra.
Zero stars, zero dependents means no Stack Overflow corpus. When something breaks, you will read source, not documentation. Budget half a day to reach a first working table, but one to two days to reach confidence on authorization and query shape — that second half is the part no README replaces.
Pin to a commit, not a version range. With no visible release cadence, ^1.0 may point at a stale branch. In composer.json:
"sigmasoft/datatable-bundle": "dev-main#abc1234"
This is the cheapest form of supply-chain discipline available for an unaudited package.
Watch the total-count query. Most datatable implementations run a SELECT count(*) for recordsTotal and another for recordsFiltered. On a wide, unfiltered table with millions of rows this dominates latency. Log both queries in local dev and check timing:
DB::listen(fn ($q) => logger()->debug($q->sql, $q->bindings));
If counts are slow, either constrain the table to an indexed filter or add a ->simplePaginate()-style mode if the package supports it. Assume no cursor pagination unless the source says otherwise.
Eager-load everything the formatter touches. A formatter accessing $row->roles->pluck('name') will silently N+1 the moment a list has more than a handful of rows. ->with() in query() is not optional; it is the difference between a 40ms and a 4s page.
Column names are the attack surface. Packages in this space frequently accept column identifiers/sort keys from the client and pass them straight into orderBy/select. Verify whitelisting in the source before any non-internal use. If the whitelist is derived from your columns() config, you are probably fine; if it trusts request('columns'), it is not. Test with a crafted request:
GET /admin/users/datatable?columns[0][data]=id,(select 1)
Don't use it for customer-facing or high-traffic surfaces. This is a developer-productivity tool for internal list views, not a performance layer. Anything with SLA implications goes behind either an internal abstraction you already own, or a mature alternative.
Debugging tips:
$request->all() inside the datatable endpoint in a local environment to see exactly what the client is sending — you will usually find that the "extra" parameters are columns you haven't whitelisted.draw, data, recordsTotal, recordsFiltered), the table renders as "Loading…" forever. Compare the response with the DataTables.net server-side protocol doc and adapt in a subclass rather than patching the vendor directory.Extension points to look for in the source, in priority order:
columns() — the safe path.id.If only one or two of those exist, the package's day-to-day value is a per-table JSON endpoint helper — nothing more. Treat it that way: adopt it as a helper, not as the data layer. And keep a hand-rolled fallback in your notes:
// Fallback: 30 lines of query builder + a Blade component
$query = User::query()->select([...]);
if ($search = request('search')) { $query->where(fn($q) => $q->where('name','like',"%$search%")->orWhere(...)); }
$rows = $query->orderBy(request('sort','id'), request('dir','asc'))->paginate(request('per_page', 25));
return response()->json(['data' => $rows->items(), 'recordsTotal' => $rows->total(), ...]);
If that fallback is genuinely comparable, choose it — it eliminates the dependency risk entirely for the same amount of code.
How can I help you explore Laravel packages today?