pierres/datatables-api-bundle
Symfony bundle that exposes DataTables-compatible API endpoints for server-side processing, with filtering, sorting, pagination, and JSON responses for Doctrine-backed datasets.
The first thing a Laravel developer must internalize: this is a Symfony bundle, not a Laravel package. There is no service provider, no php artisan vendor:publish, no merged config/datatables.php. Laravel does not load Symfony bundles — it consumes Symfony components. So "getting started" means one of two paths, and you should pick deliberately:
yajra/laravel-datatables).pierres/datatables-api core (PSR-7/PSR-15 middleware + a Doctrine-oriented query builder). The portable asset is the core, not the bundle.Minimal steps for either path:
serverSide: true request:{ "draw": 3, "recordsTotal": 10000, "recordsFiltered": 412, "data": [ ... ] }
and one request shape it consumes: draw, start, length, search[value], columns[i][data], columns[i][search][value], order[i][column], order[i][dir]. If you understand that, you can debug or reimplement anything here.
Where to look first in the repo. In order of usefulness for an outsider:
DependencyInjection/ and Resources/config/services.* — the actual source of truth for what gets wired; README examples for bundles this old are often stale.composer.json of both packages — check declared PHP/Symfony/PSR versions before anything else.Confirm the environment. composer why-not against your PHP constraint. Expect Symfony 3/4-era lineage; assume you may need to fork with widened constraints before the core even loads under PHP 8.2+.
First use case (Laravel). A read-only admin/reporting list — say /admin/orders — with an Eloquent or Query Builder source, paging/ordering/global+column search, behind a feature flag. This is the lowest-risk way to prove the pattern before expanding.
Frontend. Unchanged from any DataTables setup: serverSide: true, processing: true, ajax: { url: '/admin/orders' }. Note the DataTables.js 1.10 vs 2.x parameter contract differs enough to break parsing — pin the client version.
Interface boundary above everything. Whatever you build — port or in-house — put it behind a DataTablesQuery interface with an Eloquent implementation and (if you port the core) a Doctrine one. Nobody upstream is maintaining this; the abstraction is what makes swapping to Yajra or a bespoke implementation a contained change instead of a rewrite.
interface DataTablesQuery
{
public function paginate(DataTablesRequest $request): DataTablesResponse;
}
Request DTO, not raw arrays. Parse Illuminate\Http\Request into a typed DTO at the edge (draw, offset, limit, orders[], columns[], searches[]). The DTO is your validation and whitelisting surface. Everything downstream trusts it.
Whitelisting is the pattern, not a nice-to-have. DataTables maps client-supplied column names and order indices onto query construction. Never interpolate columns[i][data] into SQL. Each table definition maps a public column key to a real column/relation:
final class OrdersTable implements TableDefinition
{
public function columns(): array
{
return [
'id' => Column::make('orders.id')->sortable(),
'customer' => Column::make('customers.name')->relation('customer')->sortable()->searchable(),
'total' => Column::make('orders.total_cents')->sortable(),
];
}
public function baseQuery(): Builder
{
return Order::query()->with('customer'); // eager loads pre-declared
}
}
One definition class per entity. Users, orders, invoices, audit logs — each gets its own definition class holding the base query, the column map, default sort, and searchable relations. Keeps review surface small and onboarding trivial.
Filtered vs. unfiltered counts. Compute recordsTotal on the untouched base query and recordsFiltered on a clone with search applied before limit/offset. This is where most naive implementations get it wrong and produce the classic "filtered shows 412 but pagination stops at page 2" bug.
Adapter pattern for the core's Doctrine coupling. If you port the PSR-7 core, write an Eloquent adapter that translates its query-builder calls (where, orderBy, limit, offset) onto Laravel's builder — including relation aliases (customers.name → a joined table or a whereHas-style filter). Treat the adapter as owned source, not glue.
Laravel service provider + PSR-7 bridge (Path B): register the middleware/handler in the container, bind an Illuminate\Http\Request → PSR-7 conversion (Nyholm + symfony/psr-http-message-bridge), and replace the bundle's Symfony Configuration tree with Laravel config + a FormRequest or DTO validator.
Parity test suite. For each migrated table, assert row counts, ordering stability, search semantics, and the exact JSON envelope key-for-key against the existing implementation. This suite is your migration gate and doubles as the reference doc for whoever maintains the adapter.
Sequencing that works: adapter interface → parameter-parsing proof (a single test asserting a DataTables request yields the right DTO) → one read-only endpoint behind a flag → parity tests → expand → only then deprecate the incumbent path.
Cross-check against Yajra early. If yajra/laravel-datatables meets requirements on the same endpoint shape, the build-vs-buy answer is buy. Do this comparison before you sink weeks into an adapter.
composer require this into Laravel will do nothing useful — the bundle's value-add (DI, Configuration, kernel wiring) is exactly the part that does not port.columns[i][data], order[i][column], and search[value] is client-controlled. Whitelist columns via the definition class; never build identifiers from raw input. Audit the query-building path with tests parameterized against known injection payloads.recordsTotal / recordsFiltered correctness. Recompute on clones at the right points; cache recordsTotal where the underlying set is stable (e.g. a full-table count is fine to memoize per-request, not cross-request if rows churn).length: -1 (all rows). Reject/Clamp it in the DTO — this is a cheap OOM protection on deep offsets.Resources/config/. Bundle- and core-level config are split and both stale-documented; read the actual XML/YAML, not the README.DataTablesQuery interface. If you need joins/aggregations, a custom adapter method, or row-level authorization, add it at the definition layer — never patch the core.How can I help you explore Laravel packages today?