A powerful Laravel package for building advanced DataTable functionality with column filtering, global search, custom filters, sorting, and pagination. This package provides a fluent, type-safe API for creating complex data tables with minimal code.
- 🔍 Global Search: Search across multiple columns with configurable search types
- 🎯 Column Filtering: Apply specific filters to individual columns
- đź”§ Custom Filters: Create complex custom filtering logic with callables
- 📊 Sorting: Multi-column sorting with validation
- đź“„ Pagination: Built-in pagination with customizable page sizes
- 🏗️ Fluent API: Chainable methods for clean, readable code
- 🎨 Resource Integration: Seamless integration with Laravel API resources
- 📤 Export Support: Built-in download functionality with custom mappers
- đź”’ Type Safe: Full PHP 8.1+ type hints and strict typing
- đź§Ş Well Tested: Comprehensive test suite with high code coverage
- ⚡ Performance: Optimized queries with relation-aware filtering
- PHP 8.1+
- Laravel 9.0+
You can install the package via composer:
composer require jimmyahalpara/laravel-datatable
The package will automatically register itself via Laravel's package discovery.
Optionally, you can publish the configuration file:
php artisan vendor:publish --tag="datatable-config"
Here's a simple example to get you started:
use JimmyAhalpara\LaravelDatatable\DataTableService;
use JimmyAhalpara\LaravelDatatable\Filters\ColumnFilter;
use JimmyAhalpara\LaravelDatatable\Filters\GlobalFilter;
use App\Models\User;
class UserController extends Controller
{
public function index(Request $request)
{
$users = User::query();
$dataTable = DataTableService::make($users)
->setGlobalFilters([
GlobalFilter::make('name')->type('contains'),
GlobalFilter::make('email')->type('contains'),
])
->setColumnFilters([
ColumnFilter::make('status')->type('equal'),
ColumnFilter::make('created_at')->cast(fn($date) => Carbon::parse($date)),
])
->fillFromRequest($request);
return $dataTable->render();
}
}
The package comes with a configuration file that allows you to customize default behavior:
return [
// Default number of items per page
'default_items_per_page' => 10,
// Maximum items per page (prevents performance issues)
'max_items_per_page' => 100,
// Default search type for filters
'default_search_type' => 'contains',
// Enable case insensitive search by default
'case_insensitive' => true,
// Download configuration
'download' => [
'max_execution_time' => 1800, // 30 minutes
'default_filename' => 'export',
'default_format' => 'xlsx',
],
// Cache configuration
'cache' => [
'enabled' => false,
'ttl' => 300, // 5 minutes
'prefix' => 'datatable',
],
];
Start by creating a DataTable service with an Eloquent builder:
use JimmyAhalpara\LaravelDatatable\DataTableService;
use App\Models\Post;
$posts = Post::with('user', 'categories');
$dataTable = DataTableService::make($posts);
Global filters are applied when the user performs a global search. They typically search across multiple columns:
use JimmyAhalpara\LaravelDatatable\Filters\GlobalFilter;
$dataTable->setGlobalFilters([
GlobalFilter::make('title')
->type(GlobalFilter::TYPE_CONTAINS)
->caseInsensitive(true),
GlobalFilter::make('content')
->type(GlobalFilter::TYPE_CONTAINS),
// Search in related models
GlobalFilter::make('user.name')
->type(GlobalFilter::TYPE_CONTAINS),
]);
- Search Types:
contains
,startsWith
,endsWith
,equal
- Case Sensitivity: Enable/disable case-insensitive search
- Logical Operators:
AND
,OR
(defaults toOR
for global search) - Value Casting: Transform search values before applying
GlobalFilter::make('published_at')
->type('equal')
->cast(function ($value) {
return Carbon::parse($value)->format('Y-m-d');
});
Column filters are applied to specific columns based on user input:
use JimmyAhalpara\LaravelDatatable\Filters\ColumnFilter;
$dataTable->setColumnFilters([
// Simple equality filter
ColumnFilter::make('status')
->type(ColumnFilter::TYPE_EQUAL),
// Case-insensitive contains filter
ColumnFilter::make('title')
->type(ColumnFilter::TYPE_CONTAINS)
->caseInsensitive(true),
// Filter with value transformation
ColumnFilter::make('price')
->type(ColumnFilter::TYPE_EQUAL)
->cast(fn($value) => (float) $value),
// Relation filtering
ColumnFilter::make('category.name')
->type(ColumnFilter::TYPE_EQUAL),
]);
Array Value Support: Column filters automatically handle array values using IN
clauses:
// Request: filter[status][] = ['active', 'pending']
// Generates: WHERE status IN ('active', 'pending')
Relation Support: Filter on related model columns:
// This will use whereHas() automatically
ColumnFilter::make('user.email')
->type(ColumnFilter::TYPE_CONTAINS)
For complex filtering logic, use custom filters with callables:
use JimmyAhalpara\LaravelDatatable\Filters\CustomFilter;
$dataTable->setCustomFilters([
CustomFilter::make(function ($builder, $requestData) {
// Age range filter
if (isset($requestData['age_min'], $requestData['age_max'])) {
$builder->whereBetween('age', [
$requestData['age_min'],
$requestData['age_max']
]);
}
// Complex date filtering
if (isset($requestData['date_range'])) {
[$start, $end] = explode(' to ', $requestData['date_range']);
$builder->whereBetween('created_at', [
Carbon::parse($start)->startOfDay(),
Carbon::parse($end)->endOfDay(),
]);
}
// Conditional filtering
if (isset($requestData['include_archived']) && !$requestData['include_archived']) {
$builder->whereNull('archived_at');
}
}),
]);
Configure multi-column sorting with validation:
$dataTable->setSortBy([
['key' => 'created_at', 'order' => 'desc'],
['key' => 'name', 'order' => 'asc'],
]);
Sorting can also be handled automatically from request parameters:
// Frontend request
{
"sortBy": [
{"key": "name", "order": "asc"},
{"key": "created_at", "order": "desc"}
]
}
Control pagination settings:
$dataTable
->setPage(1)
->setItemsPerPage(25);
The service automatically validates pagination parameters and applies limits based on configuration.
Integrate with Laravel API Resources for consistent JSON responses:
use App\Http\Resources\PostResource;
$dataTable
->setResourceClass(PostResource::class)
->fillFromRequest($request);
return $dataTable->render();
This returns a structured response:
{
"current_page": 1,
"data": [...],
"first_page_url": "http://localhost/posts?page=1",
"from": 1,
"last_page": 5,
"last_page_url": "http://localhost/posts?page=5",
"next_page_url": "http://localhost/posts?page=2",
"path": "http://localhost/posts",
"per_page": 10,
"prev_page_url": null,
"to": 10,
"total": 50
}
Enable data export with custom formatting:
$dataTable
->setDownloadColumns(['name', 'email', 'created_at', 'status'])
->setDownloadMapper(function ($item) {
return [
'Name' => $item->name,
'Email' => $item->email,
'Registration Date' => $item->created_at->format('Y-m-d H:i:s'),
'Status' => ucfirst($item->status),
];
});
// Check if download is requested
if ($dataTable->expectsDownload()) {
return $dataTable->render(); // Returns download response
}
Automatically populate the DataTable from HTTP requests:
// The request can contain:
// - page: Page number
// - itemsPerPage: Items per page
// - sortBy: Array of sorting configurations
// - filter: Object containing all filter values
// - download: Boolean flag for export
$dataTable->fillFromRequest($request);
return $dataTable->render();
Example request structure:
{
"page": 2,
"itemsPerPage": 25,
"sortBy": [
{"key": "name", "order": "asc"}
],
"filter": {
"search": "john doe",
"status": "active",
"category_id": [1, 2, 3],
"date_range": "2023-01-01 to 2023-12-31"
},
"download": false
}
The package supports full method chaining for clean, readable code:
return DataTableService::make(User::with('posts'))
->setGlobalFilters([
GlobalFilter::make('name')->type('contains'),
GlobalFilter::make('email')->type('contains'),
])
->setColumnFilters([
ColumnFilter::make('status')->type('equal'),
ColumnFilter::make('posts.title')->type('contains'),
])
->setCustomFilters([
CustomFilter::make(function ($builder, $data) {
if (isset($data['has_posts'])) {
$builder->has('posts');
}
}),
])
->setSortBy([['key' => 'created_at', 'order' => 'desc']])
->setItemsPerPage(50)
->setResourceClass(UserResource::class)
->setDownloadColumns(['name', 'email', 'posts_count'])
->fillFromRequest($request)
->render();
class ProductController extends Controller
{
public function index(Request $request)
{
$products = Product::with(['category', 'brand', 'reviews']);
$dataTable = DataTableService::make($products)
->setGlobalFilters([
GlobalFilter::make('name')->type('contains'),
GlobalFilter::make('description')->type('contains'),
GlobalFilter::make('sku')->type('startsWith'),
GlobalFilter::make('category.name')->type('contains'),
GlobalFilter::make('brand.name')->type('contains'),
])
->setColumnFilters([
ColumnFilter::make('category_id')->type('equal'),
ColumnFilter::make('brand_id')->type('equal'),
ColumnFilter::make('status')
->type('equal')
->caseInsensitive(false),
ColumnFilter::make('price')
->type('equal')
->cast(fn($value) => (float) $value),
ColumnFilter::make('is_featured')
->type('equal')
->cast(fn($value) => (bool) $value),
])
->setCustomFilters([
CustomFilter::make(function ($builder, $data) {
// Price range filter
if (isset($data['price_min'])) {
$builder->where('price', '>=', (float) $data['price_min']);
}
if (isset($data['price_max'])) {
$builder->where('price', '<=', (float) $data['price_max']);
}
// Rating filter
if (isset($data['min_rating'])) {
$builder->whereHas('reviews', function ($q) use ($data) {
$q->havingRaw('AVG(rating) >= ?', [(float) $data['min_rating']]);
});
}
// Availability filter
if (isset($data['in_stock']) && $data['in_stock']) {
$builder->where('stock_quantity', '>', 0);
}
// Date range filter
if (isset($data['created_from'])) {
$builder->whereDate('created_at', '>=', $data['created_from']);
}
if (isset($data['created_to'])) {
$builder->whereDate('created_at', '<=', $data['created_to']);
}
}),
])
->setResourceClass(ProductResource::class)
->setDownloadColumns([
'name', 'sku', 'category.name', 'brand.name',
'price', 'stock_quantity', 'status', 'created_at'
])
->setDownloadMapper(function ($product) {
return [
'Product Name' => $product->name,
'SKU' => $product->sku,
'Category' => $product->category->name ?? 'N/A',
'Brand' => $product->brand->name ?? 'N/A',
'Price' => '$' . number_format($product->price, 2),
'Stock' => $product->stock_quantity,
'Status' => ucfirst($product->status),
'Created Date' => $product->created_at->format('Y-m-d'),
'Average Rating' => $product->reviews_avg_rating ?
round($product->reviews_avg_rating, 1) . '/5' : 'No reviews',
];
})
->fillFromRequest($request);
return $dataTable->render();
}
}
You can use the facade for a more concise syntax:
use JimmyAhalpara\LaravelDatatable\Facades\DataTable;
return DataTable::make(User::query())
->setGlobalFilters([
GlobalFilter::make('name')->type('contains')
])
->fillFromRequest($request)
->render();
The package provides comprehensive validation and error handling:
try {
$dataTable = DataTableService::make($builder)
->setPage(-1); // Will throw InvalidArgumentException
} catch (\InvalidArgumentException $e) {
// Handle validation error
return response()->json(['error' => $e->getMessage()], 400);
}
Common validation errors:
- Invalid page numbers (< 1)
- Items per page exceeding maximum limit
- Invalid sort order (not 'asc' or 'desc')
- Non-existent resource classes
- Invalid filter instances
- Use
with()
to eager load relationships when using relation filters - Add database indexes on frequently filtered columns
- Consider using
select()
to limit retrieved columns - Use
chunk()
for large exports
Enable query result caching in the configuration:
'cache' => [
'enabled' => true,
'ttl' => 300, // 5 minutes
'prefix' => 'datatable',
],
For large datasets:
- Set appropriate
max_items_per_page
limits - Use streaming for large exports
- Implement pagination limits based on user roles
The package includes comprehensive tests. Run them with:
composer test
Generate coverage report:
composer test-coverage
make(Builder $builder): self
- Create new instancesetPage(int $page): self
- Set current pagesetItemsPerPage(int $itemsPerPage): self
- Set items per pagesetSortBy(array $sortBy): self
- Set sorting configurationsetResourceClass(string $resourceClass): self
- Set API resource classsetGlobalFilters(array $filters): self
- Set global search filterssetColumnFilters(array $filters): self
- Set column-specific filterssetCustomFilters(array $filters): self
- Set custom filterssetDownloadColumns(array $columns): self
- Set export columnssetDownloadMapper(callable $mapper): self
- Set export data mapperfillFromRequest(Request $request): self
- Fill from HTTP requestapply(): LengthAwarePaginator
- Apply filters and get paginated resultsexpectsDownload(): bool
- Check if download is requestedrender(): mixed
- Render final response
TYPE_STARTS_WITH
- Starts with searchTYPE_ENDS_WITH
- Ends with searchTYPE_CONTAINS
- Contains search (default)TYPE_EQUAL
- Exact matchTYPE_LOGICAL_AND
- AND operatorTYPE_LOGICAL_OR
- OR operator (default)
make(string $columnKey): self
- Create new global filtertype(string $type): self
- Set search typecaseInsensitive(bool $caseInsensitive = true): self
- Set case sensitivitycast(callable $castCallable): self
- Set value transformerlogical(string $operator): self
- Set logical operator
Same interface as GlobalFilter with additional array value support.
make(callable $callable): self
- Create new custom filterapply($builder, $data): void
- Apply filter to query builder
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.