jimmyahalpara / laravel-datatable
A powerful Laravel package for building advanced DataTable filters with column filtering, global search, and custom filters
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/jimmyahalpara/laravel-datatable
Requires
- php: ^8.1
- illuminate/contracts: ^9.0|^10.0|^11.0
- illuminate/database: ^9.0|^10.0|^11.0
- illuminate/http: ^9.0|^10.0|^11.0
- illuminate/support: ^9.0|^10.0|^11.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^7.0|^8.0|^9.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^9.6|^10.0
README
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.
Features
- ๐ 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
Requirements
- PHP 8.1+
- Laravel 9.0+
Installation
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"
Quick Start
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(); } }
Configuration
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', ], ];
Usage Guide
Creating a DataTable Service
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
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), ]);
Global Filter Options
- 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
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), ]);
Column Filter Features
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)
Custom Filters
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'); } }), ]);
Sorting
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"} ] }
Pagination
Control pagination settings:
$dataTable ->setPage(1) ->setItemsPerPage(25);
The service automatically validates pagination parameters and applies limits based on configuration.
Resource Integration
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 }
Export/Download Functionality
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 }
Request Integration
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 }
Advanced Usage
Method Chaining
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();
Complex Filtering Example
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(); } }
Facade Usage
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();
Error Handling
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
Performance Considerations
Query Optimization
- 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
Caching
Enable query result caching in the configuration:
'cache' => [ 'enabled' => true, 'ttl' => 300, // 5 minutes 'prefix' => 'datatable', ],
Memory Management
For large datasets:
- Set appropriate
max_items_per_page
limits - Use streaming for large exports
- Implement pagination limits based on user roles
Testing
The package includes comprehensive tests. Run them with:
composer test
Generate coverage report:
composer test-coverage
API Reference
DataTableService
Methods
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
GlobalFilter
Constants
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)
Methods
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
ColumnFilter
Same interface as GlobalFilter with additional array value support.
CustomFilter
Methods
make(callable $callable): self
- Create new custom filterapply($builder, $data): void
- Apply filter to query builder
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.