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 Datatables Laravel Package

yajra/laravel-datatables

Complete installer for Laravel DataTables core and plugins, offering seamless server-side DataTables integration for Laravel. Supports DataTables 2.x, Editor 2.x, Buttons, and Select extensions, simplifying dynamic AJAX-powered data grids.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the metapackage (or just the core).

composer require yajra/laravel-datatables:^13

This pulls laravel-datatables-oracle (the query layer) plus the Buttons, Select, Editor, and Fractal plugins. If you don't need Editor or exports, install yajra/laravel-datatables-oracle alone — the day-to-day API is identical, and you halve your upgrade surface when Laravel majors move.

Service providers are auto-discovered on Laravel 11+. Publish config and assets only when you need to change defaults:

php artisan vendor:publish --provider="Yajra\DataTables\DataTablesServiceProvider"

Where to look first.

  1. config/datatables.php — engines, search.smart, search.case_insensitive, builder defaults. Read this before you write a single class; most "why doesn't search work" questions are answered here.
  2. vendor/yajra/laravel-datatables-oracle/src/Processors/ — the JSON envelope shape (draw, recordsTotal, recordsFiltered, data).
  3. The docs at yajrabox.com/docs/laravel-datatables, specifically the Eloquent engine and Column API pages.

First use case: replace a hand-rolled admin index.

Scaffold a class, then let the controller render it:

php artisan datatables:make UsersDataTable
// app/DataTables/UsersDataTable.php
class UsersDataTable extends DataTable
{
    public function query(User $model): QueryBuilder
    {
        return $model->newQuery()->with('roles')->select('users.*');
    }

    public function html(): HtmlBuilder
    {
        return $this->builder()
            ->setTableId('users-table')
            ->columns($this->getColumns())
            ->minifiedAjax()
            ->orderBy(0, 'asc');
    }

    protected function getColumns(): array
    {
        return [
            Column::make('id')->width(60),
            Column::make('name'),
            Column::make('email'),
            Column::computed('action')
                ->exportable(false)->printable(false)->width(80)->addClass('text-center'),
        ];
    }
}
// controller
public function index(UsersDataTable $dataTable)
{
    $this->authorize('viewAny', User::class);

    return $dataTable->render('users.index');
}
{{ $dataTable->table() }}

@push('scripts')
    {{ $dataTable->scripts() }}
@endpush

That is the whole loop: one class, one controller line, one Blade directive. Everything else in this package is refinement of those three pieces.

If you only need a quick endpoint and don't want a class, the inline form works:

Route::get('users/data', fn () => DataTables::eloquent(User::query())->toJson());

Implementation Patterns

Pick the engine explicitly, and let of() auto-detect as a fallback.

DataTables::eloquent(User::query())   // Eloquent builder, supports with()/scopes
DataTables::query(DB::table('users')) // raw query builder
DataTables::collection($rows)         // in-memory; no SQL, no pagination pushdown

DataTables::of($source) detects all three, but explicit is better: it documents intent and prevents a refactor from silently switching engines (which changes search/order semantics).

Scope authorization inside query(), not in the Blade template. The endpoint is a public JSON surface; every filter, sort, and search the client sends is attacker-influenced. The only place tenant/user isolation is guaranteed is the base query:

public function query(Invoice $model): QueryBuilder
{
    return $model->newQuery()
        ->whereBelongsTo(auth()->user()->organization)
        ->select('invoices.*');
}

Use filterColumn for anything that isn't a plain column. Relation search and computed columns never work by accident:

DataTables::eloquent(User::with('roles'))
    ->filterColumn('roles.name', fn ($q, $kw) => $q->whereHas('roles', fn ($r) => $r->where('name', 'like', "%{$kw}%")))
    ->orderColumn('roles.name', fn ($q, $dir) => $q->orderBy(Role::select('name')->whereColumn('roles.id', 'role_user.role_id'), $dir))
    ->editColumn('created_at', fn (User $u) => $u->created_at->toFormattedDateString())
    ->addColumn('action', fn (User $u) => view('users.actions', compact('u'))->render())
    ->rawColumns(['action'])
    ->addIndexColumn();

Custom filters from request params. filter() runs once with access to the whole request; $globalSearch = true makes it participate in the search box instead:

->filter(function ($query) {
    if ($status = request('status')) {
        $query->where('status', $status);
    }
}, true)

Keep a base DataTable class. After two screens you will have copy-pasted the same builder chain. Codify: table id convention, minifiedAjax(), responsive, default dom/layout, standard export buttons, standard date formatting, and a filename() implementation. New screens then only define query() and getColumns().

Buttons and export. Since v10.1 the export package ships by default:

->buttons([
    Button::make('excel'), Button::make('csv'), Button::make('pdf'),
    Button::make('print'), Button::make('reload'),
])
->parameters(['dom' => 'lfrtip'])  // DataTables 2.x => use 'layout' instead

Exports respect exportable(false) on columns, so hide the action column rather than stripping it after the fact.

Editor for inline CRUD requires the separate yajra/laravel-datatables-editor package plus a commercial DataTables Editor license. Model it as a normal Laravel validation problem — rules live in the Editor config, not the JS.

Testing the endpoint. Drive it like a client, not like a unit test:

$this->actingAs($user)
    ->get(route('users.data', [
        'draw' => 1, 'start' => 0, 'length' => 10,
        'columns' => [...], 'order' => [['column' => 0, 'dir' => 'asc']],
    ]))
    ->assertOk()
    ->assertJsonStructure(['draw', 'recordsTotal', 'recordsFiltered', 'data'])
    ->assertJsonCount(10, 'data');

Add one test per screen asserting that a user from another tenant gets zero rows. That single assertion is worth more than any amount of UI testing.

Gotchas and Tips

Version lock-step is the real dependency risk. Package major == Laravel major (v13 ↔ Laravel 13). Every framework upgrade becomes a coordinated bump. Pin the version, name an owner, and add a line to your Laravel upgrade checklist. Choosing an older major to dodge an upgrade is legitimate but buys a shorter runway with a date attached.

Counts lie on groupBy / having / distinct queries. recordsTotal is derived from a count query that doesn't survive those constructs. Fix with ->skipTotalRecords() and set the values yourself, or restructure the query to avoid grouping in the base.

Column::make('relation.column') does not search or sort. It renders if the attribute exists, but ordering and searching silently do nothing. Always pair with filterColumn/orderColumn, or use editColumn on a real column plus addColumn for display.

rawColumns is mandatory for HTML. Action buttons and badge markup will render as escaped text otherwise. Conversely, never rawColumns a user-supplied field — that is a stored XSS vector.

Computed columns break ordering. A column with no backing SQL column will throw or produce nonsense when the user clicks the header. Mark it ->orderable(false) or supply orderColumn.

Global search behaves differently than you expect. config('datatables.search.smart') splits the keyword on spaces and ANDs the terms; case_insensitive toggles LOWER() wrapping. On Postgres, LIKE is case-sensitive — override with filterColumn and ILIKE, or set case_insensitive globally. Test search on every database you deploy to, not just your local MySQL.

"Invalid JSON response" in the browser is almost never this package. It is a PHP exception rendering an HTML error page. Open the network tab, look at the raw body, check storage/logs/laravel.log. Common causes: a missing rawColumns, an undefined relationship in a view, or a 403 from a policy.

Multiple tables on one page need distinct table IDs, distinct ajax routes, and their own @push('scripts') blocks. Two $dataTable->scripts() calls inside the same push will collide on the DataTables global config.

minifiedAjax() matters on wide tables. Without it, every column definition is serialized into the ajax URL as a query string, which blows past URL length limits on screens with 20+ columns.

Watch the query cost, not the response size. The package runs three queries per request (count, filtered count, paginated rows). On a large table with an expensive whereHas, the counts dominate. Measure with Laravel Debugbar or DB::listen before shipping, and consider ->setTotalRecords() with a cached count for very large, rarely-changing tables.

Exports are memory-bound. Button::make('excel') builds the whole file in PHP memory. Above ~20k rows, move exports to a queued job using chunk() and a streamed writer; the DataTables export button is fine for a few thousand rows and a liability beyond that.

Prefer the core package if you use only the grid. The metapackage's value is Editor, Buttons, Select, and Fractal. If you have a design system and no inline editing, take laravel-datatables-oracle alone and keep the JSON contract — same developer experience, materially less to maintain.

Debugging tip: drop the frontend. Hit the ajax route directly with a minimal payload (?draw=1&start=0&length=10) and read the JSON. Half of all reported issues are frontend config (DataTables 2.x uses layout where 1.x used dom; Buttons and Select options moved), not server-side. Isolating that first saves an hour every time.

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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core