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

Laratables Laravel Package

freshbitsweb/laratables

freshbitsweb/laratables simplifies Laravel DataTables server-side processing with Eloquent support, relationship search, eager loading, sorting, and pagination, helping you build fast, interactive data tables with minimal code and less boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

Laratables is a thin server-side adapter between DataTables' Ajax protocol and Eloquent. It does one job: take a request (with draw, start, length, search, order, columns) and return {draw, recordsTotal, recordsFiltered, data} — including search, ordering, and pagination — without you hand-writing that JSON.

Install and publish config:

composer require freshbitsweb/laratables
php artisan vendor:publish --tag=laratables

Auto-discovery handles the service provider on 5.5+. The published config/laratables.php is tiny — the key you actually care about is max_records, a hard cap on the result set used as a safety valve against a length: -1 (or maliciously large) request.

First use case — a paginated admin index. One route, one line in the controller:

use Freshbitsweb\Laratables\Laratables;

Route::get('/admin/users', fn () => Laratables::records(User::class));

Then in Blade, point DataTables at it. The columns array is the contract — each data key must exist in the rows the endpoint returns:

$('#users').DataTable({
    processing: true,
    serverSide: true,
    ajax: '/admin/users',
    columns: [
        { data: 'id' },
        { data: 'name', name: 'name' },
        { data: 'email' },
    ],
});

Where to look first: the model class you passed in. Laratables is convention-driven — it reflects on the model for laratables* static methods to customize the query, add computed columns, and eager-load relations. Reading those conventions (in the model or the package's Laratables class) tells you more than the readme does. The second place to look is the actual JSON response: curl 'http://app.test/admin/users?draw=1&start=0&length=10' before you ever open the browser, so you can separate "backend is wrong" from "DataTables config is wrong".

If you outgrow the class-driven style, there's a fluent form for ad-hoc tables:

return Laratables::of(User::query())
    ->addColumn('action', fn ($user) => view('users.actions', compact('user'))->render())
    ->toJson();

Implementation Patterns

Model hooks are the primary extension point. Instead of configuring everything at the call site, you teach the model how it should be tabulated:

class User extends Model
{
    // Scope every table request. This is the single most important hook.
    public static function laratablesQueryConditions($query)
    {
        return $query->where('tenant_id', auth()->user()->tenant_id);
    }

    // Relation loading — kill the N+1 before it happens.
    public static function laratablesEagerLoads()
    {
        return ['roles', 'company'];
    }

    // Convention: laratablesCustom{StudlyColumn} overrides the column value.
    public static function laratablesCustomName($user)
    {
        return e("{$user->first_name} {$user->last_name}");
    }

    public static function laratablesCustomRoles($user)
    {
        return $user->roles->pluck('name')->implode(', ');
    }
}

Actions column. The standard pattern is a custom column that renders a Blade partial and marks it unsortable/unesearchable on the client:

public static function laratablesCustomAction($user)
{
    return view('admin.users.actions', ['user' => $user])->render();
}
{ data: 'action', name: 'action', orderable: false, searchable: false }

Whitelist searchable and orderable columns. DataTables sends columns[i][searchable] and order[0][column], and Laratables maps those indexes back to column names. On a table with joins or aliased columns, an unguarded order clause is a SQLSTATE[42S22] unknown column waiting to happen. Constrain it explicitly if your version exposes the searchable/orderable hooks; otherwise validate with a laratablesQueryConditions that inspects the request and falls back to id.

Relation columns. For a company.name column you generally need three things together: a join (or with()) in the query conditions, a qualified column in the searchable/orderable list, and a custom column that renders the value. Do all three or you get partial behavior — sorts that silently do nothing, or searches that match the wrong column.

Authorization is your job, not the package's. Laratables does not apply policies or global scopes differently from any other query. Treat the endpoint as a raw query surface: apply tenant scoping, soft-delete awareness, and role checks inside laratablesQueryConditions, and never accept a model class from user input.

Testing. Assert the envelope, not the whole payload:

$this->actingAs($admin)
    ->getJson('/admin/users?draw=1&start=0&length=10&search[value]=alice')
    ->assertJsonStructure(['draw', 'recordsTotal', 'recordsFiltered', 'data'])
    ->assertJsonPath('draw', 1);

Also assert scoping: log in as a second tenant and confirm recordsTotal drops. That's the test that catches the regression everyone ships.

Gotchas and Tips

  • recordsTotal vs recordsFiltered diverge for a reason. recordsTotal is the unfiltered count, recordsFiltered is after search. If they're equal when you searched, your search isn't hitting the columns you think it is. If they're both wrong, you have a groupBy/having in the query — Laravel's count() on a grouped query returns the wrong number, and Laratables will happily serialize it.
  • The max_records cap is silent. Exceed it and you get a truncated data array with no error. Admins who sort by "newest" and scroll are the ones who notice.
  • Custom columns replace DB columns of the same name. laratablesCustomName doesn't augment name; it overwrites it in the payload. That's usually what you want, but it means the raw value is no longer available to the front end — expose it under a second key if you need both.
  • $appends and accessors leak into the payload. Every attribute on the model gets serialized. A model with an expensive accessor appended will run that accessor N times per page. select() a narrower column set in the query conditions and prune $appends for table-shaped models.
  • Don't wrap the response. Returning Laratables::records(...) is already a JSON response. Wrapping it in response()->json(...) or an API Resource breaks the draw counter and DataTables will warn about a malformed response.
  • It's a GET endpoint by default. If you configure DataTables with type: 'POST', either add the route to a CSRF-exempt group or send X-CSRF-TOKEN — otherwise you'll get a 419 and DataTables will show "processing" forever with no clue why.
  • Dates are serialized as-is. created_at arrives as an ISO timestamp (or whatever your cast produces), not a formatted string. Format it with a custom column server-side, or use DataTables' render client-side — but not both, or you'll double-format.
  • Debugging loop: hit the route with curl and the real DataTables query string, inspect the raw JSON, and check the query count in Debugbar or Telescope. Almost every "Laratables is broken" report is either a data key mismatch on the client or an N+1 you forgot to eager-load.
  • Version drift is real. The entry point and hook names have shifted across majors (records / recordsOf / recordsOfClass, and various relation-loading hook names). Blog snippets from older majors will 500 on a fresh install. Grep the installed Laratables class for the actual method names before copying anything, and pin the major in composer.json — the package tracks Laravel 5.5 through 11.x, so a Laravel upgrade can move you a major without you noticing.
  • Scope check, every time. The fluent Laratables::of($query) path is where people accidentally pass an unscoped User::query() and expose the whole table. Any table endpoint needs a feature test asserting tenant isolation, because nothing in the package enforces it for you.
  • When to reach for something else: Laratables is great for straightforward admin CRUD with a handful of columns. If you need complex multi-column filter panels, CSV/Excel export, row grouping, or heavy aggregation over large tables, [yajra/laravel-datatables] has a much deeper feature set — the tradeoff is more configuration surface versus Laratables' small, convention-based one.
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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo