hasanalyazidi / laravel-datatables
Lightweight server-side engine for jQuery DataTables: one PHP class, one route line, one Blade tag — with Excel, CSV and PDF (Arabic/RTL-capable) exports.
Package info
github.com/HasanAlyazidi/laravel-datatables
pkg:composer/hasanalyazidi/laravel-datatables
Requires
- php: ^7.1|^8.0
- illuminate/database: ^6|^7|^8|^9|^10|^11|^12|^13
- illuminate/http: ^6|^7|^8|^9|^10|^11|^12|^13
- illuminate/routing: ^6|^7|^8|^9|^10|^11|^12|^13
- illuminate/support: ^6|^7|^8|^9|^10|^11|^12|^13
- illuminate/view: ^6|^7|^8|^9|^10|^11|^12|^13
Requires (Dev)
- laravel/framework: ^6|^7|^8|^9|^10|^11|^12|^13
- orchestra/testbench: ^4|^5|^6|^7|^8|^9|^10|^11
- phpunit/phpunit: ^8.5|^9|^10|^11|^12
Suggests
- carlos-meneses/laravel-mpdf: Required for the PDF exporters (^2.1).
- maatwebsite/excel: Required for the Excel (.xlsx) exporter (^3.1).
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-09-09 12:35:38 UTC
README
Server-side jQuery DataTables for Laravel, the easy way: one PHP class, one route line, one Blade tag.
Your table loads fast even with 100,000 rows, because the server sends only one page at a time. Search, sorting, paging, filters, and Excel / CSV / PDF downloads all work out of the box. Arabic and other RTL languages are fully supported.
// 1. One class class UsersDataTable extends DataTable { public function columns(): array { return [ Column::id(), Column::make('name'), Column::createdAt(), ]; } protected function query(): Builder { return User::query(); } }
// 2. One route line Route::dataTable('users/data', UsersDataTable::class)->name('users.data');
{{-- 3. One Blade tag --}} <x-datatable table="UsersDataTable" />
That's it. The table appears, loads its rows from the server, and has working search, sorting, paging, and an Export button.
Requirements
| What | Version |
|---|---|
| PHP | 7.1 or newer |
| Laravel | 6 to 13 |
| Bootstrap | 3, 4, or 5 |
| jQuery + DataTables | jQuery 1.7+, DataTables 1.10.8+ or 2.x |
The package brings its own copy of DataTables 2.3.6, so you do not need to download anything (see Front-end files).
Install
Step 1 — require the package:
composer require hasanalyazidi/laravel-datatables
Nothing else to register. Laravel finds the package by itself.
Step 2 — add two lines to your layout:
<head> ... @dataTablesStyles </head> <body> ... <script src="/your/jquery.js"></script> <script src="/your/bootstrap.bundle.js"></script> @dataTablesScripts </body>
@dataTablesStyles and @dataTablesScripts print the DataTables CSS and JS for your Bootstrap version, plus this package's own small script. The files are served from the package itself — no publish command, no CDN, no internet needed.
You only bring jQuery and Bootstrap, which your admin layout almost certainly has already.
Step 3 — (optional) turn on more export formats:
composer require maatwebsite/excel # adds Excel (.xlsx) composer require carlos-meneses/laravel-mpdf # adds PDF
CSV export works without installing anything. The Export button only shows formats that are actually installed.
Your first table
Create app/DataTables/UsersDataTable.php:
<?php namespace App\DataTables; use App\Models\User; use HasanAlyazidi\DataTables\Badge; use HasanAlyazidi\DataTables\Column; use HasanAlyazidi\DataTables\ColumnOrder; use HasanAlyazidi\DataTables\DataTable; use Illuminate\Database\Eloquent\Builder; class UsersDataTable extends DataTable { public function columns(): array { return [ Column::id('users.id'), Column::make('name'), Column::make('email')->default('-'), Column::badge('status', [ 1 => [Badge::SUCCESS, __('Active')], 0 => [Badge::DANGER, __('Blocked')], ]), Column::createdAt(), Column::actions()->render(function (User $user) { return view('users.actions', ['user' => $user])->render(); }), ]; } public function title(): string { return __('Users'); // used as the Excel sheet name and PDF heading } protected function query(): Builder { // Put your access rules here. The table shows ONLY what this returns. return User::query(); } protected function defaultOrder(): array { return [ColumnOrder::desc('created_at')]; } }
Add the route inside whatever middleware group protects the page:
Route::dataTable('users/data', \App\DataTables\UsersDataTable::class)->name('users.data');
Show it in a Blade view:
<x-datatable table="UsersDataTable" />
Short names are looked up under App\DataTables. A full class name works too:
<x-datatable :table="\App\DataTables\UsersDataTable::class" />
Laravel 6? It has no
<x-...>tags. Use the directive instead — it does exactly the same thing:@datatable('UsersDataTable')
Client-side tables (small tables)
For a small table you may not need the server side at all. Print the rows with Blade and add one class:
<table class="datatable table table-striped"> <thead> <tr><th>Name</th><th class="no-sort">Actions</th></tr> </thead> <tbody> @foreach ($users as $user) <tr>...</tr> @endforeach </tbody> </table>
The package script finds every <table class="datatable"> and turns it into a DataTable, with the same defaults and language as your server-side tables. Add no-sort to a header cell to stop sorting on that column.
Already have your own .datatable initialiser? Keep it — tell the package to leave those tables alone:
<script>window.laravelDataTables = { clientSide: false };</script> @dataTablesScripts
A table that is already a DataTable is never touched twice, either way.
Filters
Filters are normal inputs that you design yourself. Put them inside a container with the class datatable-filters:
<div class="row mb-3 datatable-filters"> <select name="status" class="form-select"> <option value="">{{ __('All') }}</option> <option value="1">{{ __('Active') }}</option> </select> </div> <x-datatable table="UsersDataTable" />
Then say what each filter does, in the class:
protected function filters(): array { return [ 'status' => function (Builder $query, string $value) { $query->where('users.status', (int) $value); }, ]; }
Only names listed here are accepted — anything else the browser sends is ignored. Multi-select inputs work too: name the input statuses[] and the closure receives an array.
The table reloads by itself when a filter changes, and exports include the current filters. Filters live somewhere unusual? Point the table at them: <x-datatable table="..." filters="#users-filters" />.
Search in other languages
When your data is translated, the search box should find the Arabic name even while the page is in English. Tell the package once — in AppServiceProvider::boot() — how your app stores translations:
use HasanAlyazidi\DataTables\DataTable; use HasanAlyazidi\DataTables\LocalizedSearch; // Translations in a joined table (alias "fb_trans"): DataTable::localizeSearchUsing(LocalizedSearch::join('fb_trans')); // Or several joins (current language + fallback): DataTable::localizeSearchUsing(LocalizedSearch::join('trans', 'fb_trans')); // Or columns like name_ar / name_en: DataTable::localizeSearchUsing(LocalizedSearch::suffix('ar', 'en')); // Or follow the current app language (name_ar when the app is in Arabic): DataTable::localizeSearchUsing(LocalizedSearch::suffix()); // Or anything else: DataTable::localizeSearchUsing(function (string $column) { return ['anything.'.$column]; });
Then mark the columns:
Column::make('name')->searchLocalized(),
Two more levels of control, for special cases:
// One table that stores translations differently — override in that class: protected function localizedSearchColumns(string $column): array { return ['pt.'.$column]; } // One column that needs exact columns — name them and skip the magic: Column::make('name')->searchColumns(['name_ar', 'name_en']),
The most specific one wins: exact columns → the table's override → the app-wide setting. If nothing is set and you call searchLocalized(), you get a clear error telling you what to register.
If your app has only one language, skip this whole section.
Columns
Presets:
| Preset | What it shows |
|---|---|
Column::make('name') |
The value of name, escaped |
Column::id('users.id') |
The primary key, with a translated "#" / "ID" title |
Column::index() |
Row numbers 1, 2, 3… (also in exports) |
Column::badge('status', [...]) |
A colored badge per value |
Column::date('col') / datetime('col') |
Formatted dates |
Column::createdAt() / updatedAt() |
The two Laravel timestamps, translated titles |
Column::actions() |
Edit/delete buttons — never sorted, searched, or exported |
Things you can change on any column:
| Method | Meaning |
|---|---|
->title('...') |
The header text |
->render(fn ($model) => ...) |
Build the cell HTML yourself (escape user data with e()!) |
->default('-') |
Shown when the value is empty |
->notOrderable() / ->notSearchable() |
Turn sorting / searching off |
->exported(false) |
Leave the column out of downloads |
->orderColumn('users.name') |
The real DB column for sorting |
->searchColumns([...]) |
The real DB column(s) for searching |
->searchConcat([...]) |
Search several columns as one text (first + last name) |
->searchLocalized(...) |
Search translated columns (see above) |
->className('text-center') |
CSS class for the cells |
Important: once your query joins other tables, write DB columns in full (users.id, not id).
Exports
Every table gets an Export button automatically. CSV is always available; Excel and PDF appear when their packages are installed (see Install, step 3).
- Downloads contain all matching rows — not just the visible page — with the current search, filters, and sorting.
- Excel cells are written as text, so phone numbers keep their
+and nothing runs as a formula. - CSV is protected against formula injection (the OWASP rule) and streams, so huge tables are fine.
- PDF supports Arabic shaping, custom fonts, your logo, and portrait/landscape. Very large PDF exports are heavy — thousands of rows are fine, tens of thousands belong in CSV/Excel.
Limit one table to certain formats:
protected $exporters = ['csv', 'pdf']; // or [] to turn exports off for this table
Put the button somewhere else on the page:
<x-datatable table="UsersDataTable" :export="false" /> ... <x-datatable.export table="UsersDataTable" target="#dt-usersdatatable" /> {{-- Laravel 6: @datatableExport('UsersDataTable', ['target' => '#dt-usersdatatable']) --}}
Turn everything off globally with 'exports' => ['enabled' => false] in the config.
Settings
The defaults work without any setup. To change them:
php artisan vendor:publish --tag=datatables-config
Then edit config/datatables.php:
| Key | What it does | Default |
|---|---|---|
theme |
bootstrap5, bootstrap4, or bootstrap3 markup |
bootstrap5 |
namespace |
Where short table names are looked up | App\DataTables |
assets.* |
How the front-end files are served (see below) | local |
exports.enabled |
Master switch for all exports | true |
exports.exporters |
Which formats exist at all | all four |
exports.orientation, exports.pageSize |
Paper for PDF and Excel printing | portrait, A4 |
exports.pdf.* |
PDF logo, colors, margins, fonts, memory | sensible defaults |
Per-table versions of most settings exist too — $exporters, $pageOrientation, $pageSize, $maxLength, $allowAll, $exportChunk, and the pdf*View() / logo() methods.
JavaScript defaults
Change the defaults for every table on the site — set the object before @dataTablesScripts:
<script> window.laravelDataTables = { defaults: { stateSave: true, pageLength: 50, lengthMenu: [[10, 25, 50, -1], [10, 25, 50, 'All']] }, allLabel: 'All' }; </script> @dataTablesScripts
Anything DataTables understands can go in defaults. Or pass the same array inline:
@dataTablesScripts(['defaults' => ['stateSave' => true]])
Change one table only, on its tag:
<x-datatable table="UsersDataTable" :page-length="50" :searching="false" />
Front-end files
Where the DataTables library itself comes from — assets.source in the config:
local(default): the copy bundled inside this package, served through two small routes. Works offline, always the version this package was tested with.cdn: load from cdn.datatables.net instead, version-pinned with integrity hashes.enabled => false: you load DataTables yourself in the layout (any version 1.10.8+). The directives then print only the package's own script.
Prefer real static files served by your web server? Publish them once — the directives switch to the published copies automatically:
php artisan vendor:publish --tag=datatables-vendor # the DataTables library php artisan vendor:publish --tag=datatables-assets # this package's script
The table language follows your app locale automatically (ar, fr, it, ms ship with the package; English needs no file). Point assets.language at a URL for any other language, or set it to false.
Themes
Set theme in the config to bootstrap5, bootstrap4, or bootstrap3 — the table markup, export dropdown, and badge classes all follow. One page can differ: <x-datatable table="..." theme="bootstrap3" />.
To change the markup itself:
php artisan vendor:publish --tag=datatables-views
then edit resources/views/vendor/datatables/themes/.... A whole new theme is just a new folder there with table.blade.php and export.blade.php, plus its name in the config.
Translations
The package speaks English, Arabic, French, Italian, and Malay. To change a word, you do not need to publish anything — create datatables.php in your app's lang folder (lang/xx/, or resources/lang/xx/ on Laravel 6-8) and set only the keys you want:
return [ 'export' => 'Download', ];
Or publish the full files with --tag=datatables-lang.
Troubleshooting
The table shows but never loads rows.
Open the browser console and the Network tab. A red request to your .../data route usually means the route is missing or behind the wrong middleware. (An expired session handles itself: the page reloads once and lands on your login screen.)
"laravel-datatables: jQuery DataTables is not loaded" in the console.
@dataTablesScripts must come after jQuery. If you set assets.enabled = false, your layout must load DataTables itself.
The Export button misses Excel or PDF. That format's package is not installed — see Install, step 3. This is by design: formats appear only when they can work.
Column::searchLocalized() needs a mapping...
Register DataTable::localizeSearchUsing(...) in a service provider — see Search in other languages.
SQL error "unknown column" when sorting or searching.
Your query joins tables, so a bare column name became ambiguous. Write it in full: ->orderColumn('users.name').
PDF export runs out of memory.
Raise exports.pdf.memoryLimit, or export that table as CSV/Excel — they stream and have no practical limit.
On Laravel 6 the <x-datatable> tag prints as text.
Laravel 6 has no component tags. Use @datatable('UsersDataTable').
mPDF error about a temp directory.
mPDF writes temporary files to storage/app/mpdf — make sure storage/ is writable.
Testing
composer test
Changelog / Contributing / License
See CHANGELOG.md and CONTRIBUTING.md. Open-sourced under the MIT license.