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

Laravel Pagetables Laravel Package

savannabits/laravel-pagetables

Laravel Pagetables adds clean, paginated data tables to your Laravel apps. Define columns and rows with a fluent API, then render responsive tables with built-in search, sorting, and pagination—ideal for admin panels and dashboards.

View on GitHub
Deep Wiki
Context7

Getting Started

Read src/ before you run composer require. This package is three stars, zero dependents, and a last release in mid-2022. The README describes intent; src/ describes behaviour. Ten minutes of reading gets you:

  • the class or trait that accepts a query builder and produces the DataTables JSON payload,
  • how it parses draw, start, length, search[value], order[0][column], and columns[i][data],
  • whether it accepts an Eloquent Builder, a DB query builder, or a raw array,
  • which methods are protected — those are your override points and the real API surface.

Method names in the README may not match the source. Trust the source.

Pin the version and check your framework. v1.1.0 (2022-06-13) exists solely to widen constraints for Laravel 9. Laravel 10/11/12 are unclaimed and untested; the install will fail on illuminate/* constraints. Either stay on L9, or plan to widen composer.json in a fork you own.

composer require "savannabits/laravel-pagetables:1.1.0"

First use case — a Users index. One JSON route per resource, a scoped query, and DataTables pointed at it:

Route::get('/users/table', [UserTableController::class, 'data'])->name('users.table');

public function data(Request $request)
{
    return Pagetable::make(
        User::query()
            ->select(['users.id', 'users.name', 'users.email', 'users.created_at'])
            ->where('tenant_id', auth()->user()->tenant_id)
    )
    ->columns([
        'name'       => 'Name',
        'email'      => 'Email',
        'created_at' => 'Joined',
    ])
    ->searchable(['users.name', 'users.email'])
    ->sortable(['users.name', 'users.email', 'users.created_at'])
    ->response();
}

Front end stays conventional server-side DataTables:

$('#users-table').DataTable({
  processing: true,
  serverSide: true,
  ajax: '{{ route('users.table') }}',
  columns: [
    { data: 'name' }, { data: 'email' }, { data: 'created_at' },
    { data: null, orderable: false, searchable: false,
      render: (row) => `<a href="/users/${row.id}">View</a>` }
  ]
});

Validate the payload before you touch JavaScript. Hand-craft a DataTables request and read the JSON:

curl 'http://app.test/users/table?draw=1&start=0&length=10'

Confirm draw, recordsTotal, recordsFiltered, and data are present and shaped correctly. If those four aren't right, no amount of front-end debugging will help.

Implementation Patterns

Scope the query before handing it over. Every tenancy restriction, permission check, and business filter belongs on the builder before the pagetable call. The adapter only knows about $query; it has no idea what your auth model looks like.

$query = User::query()
    ->where('tenant_id', auth()->user()->tenant_id)
    ->when($request->boolean('active_only'), fn ($q) => $q->where('active', true));

Build one column map and reuse it. Define columns once as a property, then derive both the whitelist and the response from it, so sort/search keys can't drift between the header, the JS config, and the server.

protected array $columns = [
    0 => ['data' => 'id',         'name' => 'users.id',    'sortable' => true,  'searchable' => false],
    1 => ['data' => 'name',       'name' => 'users.name',  'sortable' => true,  'searchable' => true],
    2 => ['data' => 'email',      'name' => 'users.email', 'sortable' => true,  'searchable' => true],
    3 => ['data' => 'roles.name', 'name' => 'roles.name',  'sortable' => true,  'searchable' => true],
];

Always feed the qualified DB name (users.name), never the alias. Joins are the single largest source of "ambiguous column" errors in this pattern.

Kill N+1 before it bites. Server-side tables amplify lazy loading — every page of 25 rows fires accessor queries. Eager load and compute in SQL:

User::query()
    ->select(['users.id', 'users.name', 'users.email', 'users.created_at'])
    ->with(['roles:id,name'])
    ->withCount('logins');

One route and one Pagetable class per resource. Resist the tempting "generic table endpoint parameterized by model name." That design accepts table and column names from the request, which is an SQL-injection vector you cannot meaningfully authorize. A subclass per resource with its own $columns map is the pattern that scales without becoming a liability.

Extract a BasePagetable. The package ships no opinionated filter API, so fold date-range filters, global-search overrides, default sorting, and your Laravel 10+ compatibility shim into an abstract base extending it. That base is also your migration seam: when you eventually move to yajra/laravel-datatables, only one class changes.

Format after pagination, not in the query. Add a transform()/map() over the collection after the paginate call so formatting cost is bounded to the page. Keep id in the payload even when the column is hidden — row actions need it.

->transform(fn ($row) => [
    'id'         => $row->id,
    'name'       => $row->name,
    'email'      => $row->email,
    'created_at' => $row->created_at->toFormattedDateString(),
])

Test the endpoint, not the UI. DataTables requests are just query strings, so this is a plain feature test:

$this->getJson('/users/table?draw=3&start=0&length=10&search[value]=ada&order[0][column]=1&order[0][dir]=asc')
     ->assertOk()
     ->assertJsonPath('draw', 3)
     ->assertJsonCount(1, 'data');

The assertion that catches the most real bugs: check that recordsFiltered changes when search[value] changes.

Gotchas and Tips

  • length: -1 ("All") breaks pagination. DataTables sends length=-1 for the "All" option; passed straight to paginate() you get an exception or a one-row page. Clamp and cap it: $perPage = $request->integer('length', 15); $perPage = $perPage <= 0 ? 15 : min($perPage, 100);. The cap is not optional — without it a client can request half a million rows.

  • draw must be echoed verbatim. Omit it, cast it, or truncate it and DataTables reports "Invalid JSON response" while sitting on "Processing…" forever. Check the raw curl output before opening devtools.

  • recordsTotal and recordsFiltered must come from different queries. Total before search filters, filtered after search but before limit. Making them equal makes paging controls lie; swapping them breaks the "Showing X of Y" text. Test with a search term that matches strictly fewer rows than the total.

  • Never trust columns[i][data] as a column name. If the adapter derives ORDER BY from the request, that's both an injection surface and an ambiguity hazard. Whitelist through your $columns map and abort(422) on anything unknown. Verify in src/ — if the package doesn't do this, do it in your fork.

  • LIKE %term% across joined tables is a trap. An unqualified name in a users + roles join silently hits the wrong table or throws. Qualify every searchable. LIKE is case-insensitive on MySQL's default collation but case-sensitive on Postgres — use ilike or whereRaw('lower(users.name) like ?', [...]) if you support both.

  • Sorting on a string-cast column lies. Dates stored as varchar, money as decimal, or created_at cast to string in select can sort lexicographically depending on driver. Sort on the real DB column; format only in the response.

  • POST vs GET. DataTables defaults to GET, which sidesteps CSRF entirely. If you switch to POST for long filter payloads, add the token to the ajax config or you'll get 419s that look like adapter failures.

  • Debugging recipe, in order: (1) curl the endpoint with a hand-written DataTables query string; (2) dump $query->toSql() and getBindings() immediately before pagination; (3) register DB::listen(fn ($q) => logger($q->sql)) in a service provider; (4) temporarily set serverSide: false in JS to isolate payload shape from query logic. Nine times out of ten the fault is in the column map.

  • Override, don't fork, where you can. Look for the protected methods handling search, ordering, and the two count queries. Overriding those in your BasePagetable is cheaper than fighting the request layer, and it keeps the eventual swap to a maintained adapter down to a single class.

  • Track the tag, not the branch. Record in composer.json or an ADR that you're on 1.1.0 and exactly which illuminate/* constraints you widened. When the next release arrives — likely another constraint bump — you want to diff composer.json only, not re-audit src/.

  • v1.1.0 changed nothing but composer.json. No API changes, no fixes, no deprecations. If you were already on L9 via a widened-constraint fork, upgrading does nothing. If the old constraint blocked your install, this is the unblock. Don't expect behaviour 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.
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