├── CHANGELOG.md ├── CONDUCT.md ├── LICENSE.md ├── README.md ├── UPGRADE.md ├── composer.json └── src ├── ApiResourceDataTable.php ├── CollectionDataTable.php ├── Contracts ├── DataTable.php └── Formatter.php ├── DataTableAbstract.php ├── DataTables.php ├── DataTablesServiceProvider.php ├── EloquentDataTable.php ├── Exceptions └── Exception.php ├── Facades └── DataTables.php ├── Processors ├── DataProcessor.php └── RowProcessor.php ├── QueryDataTable.php ├── Utilities ├── Config.php ├── Helper.php └── Request.php ├── config └── datatables.php ├── helper.php └── lumen.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Laravel DataTables 2 | 3 | ## CHANGELOG 4 | 5 | ### [Unreleased] 6 | 7 | ### v12.3.0 - 2025-05-17 8 | 9 | - feat: add option to enable alias on relation tables #3234 10 | - tests: Add tests to cover prefix detection #3239 11 | - fix: https://github.com/yajra/laravel-datatables/pull/1782 12 | 13 | ### v12.2.1 - 2025-05-09 14 | 15 | - fix: improve prefix detection #3238 16 | - fix: #3237 17 | 18 | ### v12.2.0 - 2025-05-08 19 | 20 | - feat: add relation resolver param to order callback #3232 21 | - fix: improve column alias detection #3236 22 | - fix: #3235 23 | 24 | ### v12.1.2 - 2025-05-07 25 | 26 | - fix: prevent prefixing null/empty string #3233 27 | 28 | ### v12.1.1 - 2025-05-05 29 | 30 | - fix: prevent ambiguous column names #3227 31 | 32 | ### v12.1.0 - 2025-04-28 33 | 34 | - feat: add relation resolver param to filter callbacks #3229 35 | 36 | ### v12.0.1 - 2025-04-07 37 | 38 | - fix: query results improvements #3224 39 | 40 | ### v12.0.0 - 2025-02-26 41 | 42 | - feat: Laravel v12 Compatibility #3217 43 | - fix: prevent duplicate table name errors #3216 44 | 45 | [Unreleased]: https://github.com/yajra/laravel-datatables/compare/v12.0.0...master 46 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. 5 | 6 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 7 | 8 | ## Our Standards 9 | Examples of behavior that contributes to a positive environment for our community include: 10 | * Demonstrating empathy and kindness toward other people 11 | * Being respectful of differing opinions, viewpoints, and experiences 12 | * Giving and gracefully accepting constructive feedback 13 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 14 | * Focusing on what is best not just for us as individuals, but for the overall community 15 | 16 | Examples of unacceptable behavior include: 17 | * The use of sexualized language or imagery, and sexual attention or advances of any kind 18 | * Trolling, insulting or derogatory comments, and personal or political attacks 19 | * Public or private harassment 20 | * Publishing others’ private information, such as a physical or email address, without their explicit permission 21 | * Other conduct which could reasonably be considered inappropriate in a professional setting 22 | 23 | ## Enforcement Responsibilities 24 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 25 | 26 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 27 | 28 | ## Scope 29 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 30 | 31 | ## Enforcement 32 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. 33 | 34 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 35 | 36 | ## Enforcement Guidelines 37 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 38 | 39 | ### 1. Correction 40 | Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 41 | 42 | Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 43 | 44 | ### 2. Warning 45 | Community Impact: A violation through a single incident or series of actions. 46 | 47 | Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 48 | 49 | ### 3. Temporary Ban 50 | Community Impact: A serious violation of community standards, including sustained inappropriate behavior. 51 | 52 | Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 53 | 54 | ### 4. Permanent Ban 55 | Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 56 | 57 | Consequence: A permanent ban from any sort of public interaction within the community. 58 | 59 | ## Attribution 60 | This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. 61 | 62 | Community Impact Guidelines were inspired by Mozilla’s code of conduct enforcement ladder. 63 | 64 | For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 65 | For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 66 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013-2022 Arjay Angeles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jQuery DataTables API for Laravel 2 | 3 | [![Join the chat at https://gitter.im/yajra/laravel-datatables](https://badges.gitter.im/yajra/laravel-datatables.svg)](https://gitter.im/yajra/laravel-datatables?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://www.paypal.me/yajra) 5 | [![Donate](https://img.shields.io/badge/donate-patreon-blue.svg)](https://www.patreon.com/bePatron?u=4521203) 6 | 7 | [![Laravel 12](https://img.shields.io/badge/Laravel-12-orange.svg)](http://laravel.com) 8 | [![Latest Stable Version](https://img.shields.io/packagist/v/yajra/laravel-datatables-oracle.svg)](https://packagist.org/packages/yajra/laravel-datatables-oracle) 9 | [![Continuous Integration](https://github.com/yajra/laravel-datatables/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/yajra/laravel-datatables/actions/workflows/continuous-integration.yml) 10 | [![Static Analysis](https://github.com/yajra/laravel-datatables/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/yajra/laravel-datatables/actions/workflows/static-analysis.yml) 11 | 12 | [![Total Downloads](https://poser.pugx.org/yajra/laravel-datatables-oracle/d/total.svg)](https://packagist.org/packages/yajra/laravel-datatables-oracle) 13 | [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://packagist.org/packages/yajra/laravel-datatables-oracle) 14 | 15 | Laravel package for handling [server-side](https://www.datatables.net/manual/server-side) works of [DataTables](http://datatables.net) jQuery Plugin via [AJAX option](https://datatables.net/reference/option/ajax) by using Eloquent ORM, Fluent Query Builder or Collection. 16 | 17 | ```php 18 | use Yajra\DataTables\Facades\DataTables; 19 | 20 | return DataTables::eloquent(User::query())->toJson(); 21 | return DataTables::query(DB::table('users'))->toJson(); 22 | return DataTables::collection(User::all())->toJson(); 23 | 24 | return DataTables::make(User::query())->toJson(); 25 | return DataTables::make(DB::table('users'))->toJson(); 26 | return DataTables::make(User::all())->toJson(); 27 | ``` 28 | 29 | ## Sponsors 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 |
35 | DataTables Logo 36 | A big thank you to DataTables for supporting this project with a free DataTables Editor license.
41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 |
46 | JetBrains logo. 47 | A big thank you to JetBrains for supporting this project with free open-source licenses of their IDEs.
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
Blackfire.io LogoA big thank you to Blackfire.io for supporting this project with a free open-source license.
61 | 62 | ## Requirements 63 | - [PHP >= 8.2](http://php.net/) 64 | - [Laravel Framework](https://github.com/laravel/framework) 65 | - [DataTables](http://datatables.net/) 66 | 67 | ## Documentations 68 | 69 | - [Github Docs](https://github.com/yajra/laravel-datatables-docs) 70 | - [Laravel DataTables Quick Starter](https://yajrabox.com/docs/laravel-datatables/master/quick-starter) 71 | - [Laravel DataTables Documentation](https://yajrabox.com/docs/laravel-datatables) 72 | 73 | ## Laravel Version Compatibility 74 | 75 | | Laravel | Package | 76 | |:--------|:---------| 77 | | 4.2.x | 3.x | 78 | | 5.0.x | 6.x | 79 | | 5.1.x | 6.x | 80 | | 5.2.x | 6.x | 81 | | 5.3.x | 6.x | 82 | | 5.4.x | 7.x, 8.x | 83 | | 5.5.x | 8.x | 84 | | 5.6.x | 8.x | 85 | | 5.7.x | 8.x | 86 | | 5.8.x | 9.x | 87 | | 6.x | 9.x | 88 | | 7.x | 9.x | 89 | | 8.x | 9.x | 90 | | 9.x | 10.x | 91 | | 10.x | 10.x | 92 | | 11.x | 11.x | 93 | | 12.x | 12.x | 94 | 95 | ## Quick Installation 96 | 97 | ### Option 1: Install all DataTables libraries 98 | 99 | ```bash 100 | composer require yajra/laravel-datatables:"^12" 101 | ``` 102 | 103 | ### Option 2: Install only this library 104 | 105 | ```bash 106 | composer require yajra/laravel-datatables-oracle:"^12" 107 | ``` 108 | 109 | #### Service Provider & Facade (Optional on Laravel 5.5+) 110 | 111 | Register the provider and facade on your `config/app.php` file. 112 | ```php 113 | 'providers' => [ 114 | ..., 115 | Yajra\DataTables\DataTablesServiceProvider::class, 116 | ] 117 | 118 | 'aliases' => [ 119 | ..., 120 | 'DataTables' => Yajra\DataTables\Facades\DataTables::class, 121 | ] 122 | ``` 123 | 124 | #### Configuration (Optional) 125 | 126 | ```bash 127 | php artisan vendor:publish --provider="Yajra\DataTables\DataTablesServiceProvider" 128 | ``` 129 | 130 | And that's it! Start building out some awesome DataTables! 131 | 132 | ## Debugging Mode 133 | 134 | To enable debugging mode, just set `APP_DEBUG=true` and the package will include the queries and inputs used when processing the table. 135 | 136 | > [!IMPORTANT] 137 | > Please ensure that the `APP_DEBUG` config is set to false when your app is in production. 138 | 139 | ## PHP ARTISAN SERVE BUG 140 | 141 | Please avoid using `php artisan serve` when developing the package. 142 | There are known bugs when using this where Laravel randomly returns a redirect and 401 (Unauthorized) if the route requires authentication and a 404 NotFoundHttpException on valid routes. 143 | 144 | It is advised to use [Homestead](https://laravel.com/docs/5.4/homestead) or [Valet](https://laravel.com/docs/5.4/valet) when working with the package. 145 | 146 | ## Contributing 147 | 148 | Please see [CONTRIBUTING](https://github.com/yajra/laravel-datatables/blob/master/.github/CONTRIBUTING.md) for details. 149 | 150 | ## Security 151 | 152 | If you discover any security-related issues, please email [aqangeles@gmail.com](mailto:aqangeles@gmail.com) instead of using the issue tracker. 153 | 154 | ## Credits 155 | 156 | - [Arjay Angeles](https://github.com/yajra) 157 | - [bllim/laravel4-datatables-package](https://github.com/bllim/laravel4-datatables-package) 158 | - [All Contributors](https://github.com/yajra/laravel-datatables/graphs/contributors) 159 | 160 | ## License 161 | 162 | The MIT License (MIT). Please see [License File](https://github.com/yajra/laravel-datatables/blob/master/LICENSE.md) for more information. 163 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # UPGRADE GUIDE 2 | 3 | ## Upgrading from v11.x to v12.x 4 | 5 | - See PR https://github.com/yajra/laravel-datatables/pull/3216 6 | 7 | ## Upgrading from v9.x to v10.x 8 | 9 | - `ApiResourceDataTable` support dropped, use `CollectionDataTable` instead. 10 | - `queryBuilder()` deprecated method removed, use `query()` instead. 11 | - Methods signature were updated to PHP8 syntax, adjust as needed if you extended the package. 12 | 13 | ## Upgrading from v8.x to v9.x 14 | 15 | No breaking changes with only a bump on php version requirements. 16 | 17 | ## Upgrading from v7.x to v8.x 18 | 19 | There are breaking changes since DataTables v8.x. If you are upgrading from v7.x to v8.x, please see [upgrade guide](https://yajrabox.com/docs/laravel-datatables/master/upgrade). 20 | 21 | ## Upgrading from v6.x to v7.x 22 | - composer require yajra/laravel-datatables-oracle 23 | - composer require yajra/laravel-datatables-buttons 24 | - php artisan vendor:publish --tag=datatables --force 25 | - php artisan vendor:publish --tag=datatables-buttons --force 26 | 27 | ## Upgrading from v5.x to v6.x 28 | - Change all occurrences of `yajra\Datatables` to `Yajra\Datatables`. (Use Sublime's find and replace all for faster update). 29 | - Remove `Datatables` facade registration. 30 | - Temporarily comment out `Yajra\Datatables\DatatablesServiceProvider`. 31 | - Update package version on your composer.json and use `yajra/laravel-datatables-oracle: ~6.0` 32 | - Uncomment the provider `Yajra\Datatables\DatatablesServiceProvider`. 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yajra/laravel-datatables-oracle", 3 | "description": "jQuery DataTables API for Laravel", 4 | "keywords": [ 5 | "yajra", 6 | "laravel", 7 | "dataTables", 8 | "jquery" 9 | ], 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Arjay Angeles", 14 | "email": "aqangeles@gmail.com" 15 | } 16 | ], 17 | "require": { 18 | "php": "^8.2", 19 | "illuminate/database": "^12", 20 | "illuminate/filesystem": "^12", 21 | "illuminate/http": "^12", 22 | "illuminate/support": "^12", 23 | "illuminate/view": "^12" 24 | }, 25 | "require-dev": { 26 | "algolia/algoliasearch-client-php": "^3.4.1", 27 | "larastan/larastan": "^3.1.0", 28 | "laravel/pint": "^1.14", 29 | "laravel/scout": "^10.8.3", 30 | "meilisearch/meilisearch-php": "^1.6.1", 31 | "orchestra/testbench": "^10", 32 | "rector/rector": "^2.0" 33 | }, 34 | "suggest": { 35 | "yajra/laravel-datatables-export": "Plugin for server-side exporting using livewire and queue worker.", 36 | "yajra/laravel-datatables-buttons": "Plugin for server-side exporting of dataTables.", 37 | "yajra/laravel-datatables-html": "Plugin for server-side HTML builder of dataTables.", 38 | "yajra/laravel-datatables-fractal": "Plugin for server-side response using Fractal.", 39 | "yajra/laravel-datatables-editor": "Plugin to use DataTables Editor (requires a license)." 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "Yajra\\DataTables\\": "src/" 44 | }, 45 | "files": [ 46 | "src/helper.php" 47 | ] 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Yajra\\DataTables\\Tests\\": "tests/" 52 | } 53 | }, 54 | "extra": { 55 | "branch-alias": { 56 | "dev-master": "12.x-dev" 57 | }, 58 | "laravel": { 59 | "providers": [ 60 | "Yajra\\DataTables\\DataTablesServiceProvider" 61 | ], 62 | "aliases": { 63 | "DataTables": "Yajra\\DataTables\\Facades\\DataTables" 64 | } 65 | } 66 | }, 67 | "config": { 68 | "sort-packages": true, 69 | "allow-plugins": { 70 | "php-http/discovery": true 71 | } 72 | }, 73 | "scripts": { 74 | "test": "./vendor/bin/phpunit", 75 | "pint": "./vendor/bin/pint", 76 | "rector": "./vendor/bin/rector", 77 | "stan": "./vendor/bin/phpstan analyse --memory-limit=2G --ansi --no-progress --no-interaction --configuration=phpstan.neon.dist", 78 | "pr": [ 79 | "@rector", 80 | "@pint", 81 | "@stan", 82 | "@test" 83 | ] 84 | }, 85 | "minimum-stability": "dev", 86 | "prefer-stable": true, 87 | "funding": [ 88 | { 89 | "type": "github", 90 | "url": "https://github.com/sponsors/yajra" 91 | } 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /src/ApiResourceDataTable.php: -------------------------------------------------------------------------------- 1 | |array $source 24 | * @return ApiResourceDataTable|DataTableAbstract 25 | */ 26 | public static function create($source) 27 | { 28 | return parent::create($source); 29 | } 30 | 31 | /** 32 | * CollectionEngine constructor. 33 | * 34 | * @param \Illuminate\Http\Resources\Json\AnonymousResourceCollection $resourceCollection 35 | */ 36 | public function __construct(AnonymousResourceCollection $resourceCollection) 37 | { 38 | /** @var \Illuminate\Support\Collection<(int|string), array> $collection */ 39 | $collection = collect($resourceCollection)->pluck('resource'); 40 | $this->request = app('datatables.request'); 41 | $this->config = app('datatables.config'); 42 | $this->collection = $collection; 43 | $this->original = $collection; 44 | $this->columns = array_keys($this->serialize($collection->first())); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/CollectionDataTable.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public Collection $original; 22 | 23 | /** 24 | * The offset of the first record in the full dataset. 25 | */ 26 | private int $offset = 0; 27 | 28 | /** 29 | * CollectionEngine constructor. 30 | * 31 | * @param \Illuminate\Support\Collection $collection 32 | */ 33 | public function __construct(public Collection $collection) 34 | { 35 | $this->request = app('datatables.request'); 36 | $this->config = app('datatables.config'); 37 | $this->original = $this->collection; 38 | $this->columns = array_keys($this->serialize($this->collection->first())); 39 | } 40 | 41 | /** 42 | * Serialize collection. 43 | */ 44 | protected function serialize(mixed $collection): array 45 | { 46 | return $collection instanceof Arrayable ? $collection->toArray() : (array) $collection; 47 | } 48 | 49 | /** 50 | * Can the DataTable engine be created with these parameters. 51 | * 52 | * @param mixed $source 53 | * @return bool 54 | */ 55 | public static function canCreate($source) 56 | { 57 | return is_array($source) || $source instanceof Collection; 58 | } 59 | 60 | /** 61 | * Factory method, create and return an instance for the DataTable engine. 62 | * 63 | * @param AnonymousResourceCollection|array|\Illuminate\Support\Collection $source 64 | * @return static 65 | */ 66 | public static function create($source) 67 | { 68 | if (is_array($source)) { 69 | $source = new Collection($source); 70 | } 71 | 72 | return parent::create($source); 73 | } 74 | 75 | /** 76 | * Count results. 77 | */ 78 | public function count(): int 79 | { 80 | return $this->collection->count(); 81 | } 82 | 83 | /** 84 | * Perform column search. 85 | */ 86 | public function columnSearch(): void 87 | { 88 | for ($i = 0, $c = count($this->request->columns()); $i < $c; $i++) { 89 | $column = $this->getColumnName($i); 90 | 91 | if (is_null($column)) { 92 | continue; 93 | } 94 | 95 | if (! $this->request->isColumnSearchable($i) || $this->isBlacklisted($column)) { 96 | continue; 97 | } 98 | 99 | $regex = $this->request->isRegex($i); 100 | $keyword = $this->request->columnKeyword($i); 101 | 102 | $this->collection = $this->collection->filter( 103 | function ($row) use ($column, $keyword, $regex) { 104 | $data = $this->serialize($row); 105 | 106 | /** @var string $value */ 107 | $value = Arr::get($data, $column); 108 | 109 | if ($this->config->isCaseInsensitive()) { 110 | if ($regex) { 111 | return preg_match('/'.$keyword.'/i', $value) == 1; 112 | } 113 | 114 | return str_contains(Str::lower($value), Str::lower($keyword)); 115 | } 116 | 117 | if ($regex) { 118 | return preg_match('/'.$keyword.'/', $value) == 1; 119 | } 120 | 121 | return str_contains($value, $keyword); 122 | } 123 | ); 124 | } 125 | } 126 | 127 | /** 128 | * Perform pagination. 129 | */ 130 | public function paging(): void 131 | { 132 | $offset = $this->request->start() - $this->offset; 133 | $length = $this->request->length() > 0 ? $this->request->length() : 10; 134 | 135 | $this->collection = $this->collection->slice($offset, $length); 136 | } 137 | 138 | /** 139 | * Organizes works. 140 | * 141 | * @throws \Exception 142 | */ 143 | public function make(bool $mDataSupport = true): JsonResponse 144 | { 145 | try { 146 | $this->totalRecords = $this->totalCount(); 147 | 148 | if ($this->totalRecords) { 149 | $results = $this->results(); 150 | $processed = $this->processResults($results, $mDataSupport); 151 | $output = $this->transform($results, $processed); 152 | 153 | $this->collection = collect($output); 154 | $this->ordering(); 155 | $this->filterRecords(); 156 | $this->paginate(); 157 | 158 | $this->revertIndexColumn($mDataSupport); 159 | } 160 | 161 | return $this->render($this->collection->values()->all()); 162 | } catch (Exception $exception) { 163 | return $this->errorResponse($exception); 164 | } 165 | } 166 | 167 | /** 168 | * Get results. 169 | * 170 | * @return \Illuminate\Support\Collection 171 | */ 172 | public function results(): Collection 173 | { 174 | return $this->collection; 175 | } 176 | 177 | /** 178 | * Revert transformed DT_RowIndex back to its original values. 179 | * 180 | * @param bool $mDataSupport 181 | */ 182 | private function revertIndexColumn($mDataSupport): void 183 | { 184 | if ($this->columnDef['index']) { 185 | $indexColumn = config('datatables.index_column', 'DT_RowIndex'); 186 | $index = $mDataSupport ? $indexColumn : 0; 187 | $start = $this->request->start(); 188 | 189 | $this->collection->transform(function ($data) use ($index, &$start) { 190 | $data[$index] = ++$start; 191 | 192 | return $data; 193 | }); 194 | } 195 | } 196 | 197 | /** 198 | * Define the offset of the first item of the collection with respect to 199 | * the FULL dataset the collection was sliced from. It effectively allows the 200 | * collection to be "pre-sliced". 201 | * 202 | * @return static 203 | */ 204 | public function setOffset(int $offset): self 205 | { 206 | $this->offset = $offset; 207 | 208 | return $this; 209 | } 210 | 211 | /** 212 | * Perform global search for the given keyword. 213 | */ 214 | protected function globalSearch(string $keyword): void 215 | { 216 | $keyword = $this->config->isCaseInsensitive() ? Str::lower($keyword) : $keyword; 217 | 218 | $this->collection = $this->collection->filter(function ($row) use ($keyword) { 219 | $data = $this->serialize($row); 220 | foreach ($this->request->searchableColumnIndex() as $index) { 221 | $column = $this->getColumnName($index); 222 | $value = Arr::get($data, $column); 223 | if (! is_string($value)) { 224 | continue; 225 | } else { 226 | $value = $this->config->isCaseInsensitive() ? Str::lower($value) : $value; 227 | } 228 | 229 | if (Str::contains($value, $keyword)) { 230 | return true; 231 | } 232 | } 233 | 234 | return false; 235 | }); 236 | } 237 | 238 | /** 239 | * Perform default query orderBy clause. 240 | */ 241 | protected function defaultOrdering(): void 242 | { 243 | $criteria = $this->request->orderableColumns(); 244 | if (! empty($criteria)) { 245 | $sorter = $this->getSorter($criteria); 246 | 247 | $this->collection = $this->collection 248 | ->map(fn ($data) => Arr::dot($data)) 249 | ->sort($sorter) 250 | ->map(function ($data) { 251 | foreach ($data as $key => $value) { 252 | unset($data[$key]); 253 | Arr::set($data, $key, $value); 254 | } 255 | 256 | return $data; 257 | }); 258 | } 259 | } 260 | 261 | /** 262 | * Get array sorter closure. 263 | */ 264 | protected function getSorter(array $criteria): Closure 265 | { 266 | return function ($a, $b) use ($criteria) { 267 | foreach ($criteria as $orderable) { 268 | $column = $this->getColumnName($orderable['column']); 269 | $direction = $orderable['direction']; 270 | if ($direction === 'desc') { 271 | $first = $b; 272 | $second = $a; 273 | } else { 274 | $first = $a; 275 | $second = $b; 276 | } 277 | if (is_numeric($first[$column] ?? null) && is_numeric($second[$column] ?? null)) { 278 | if ($first[$column] < $second[$column]) { 279 | $cmp = -1; 280 | } elseif ($first[$column] > $second[$column]) { 281 | $cmp = 1; 282 | } else { 283 | $cmp = 0; 284 | } 285 | } elseif ($this->config->isCaseInsensitive()) { 286 | $cmp = strnatcasecmp($first[$column] ?? '', $second[$column] ?? ''); 287 | } else { 288 | $cmp = strnatcmp($first[$column] ?? '', $second[$column] ?? ''); 289 | } 290 | if ($cmp != 0) { 291 | return $cmp; 292 | } 293 | } 294 | 295 | // all elements were equal 296 | return 0; 297 | }; 298 | } 299 | 300 | /** 301 | * Resolve callback parameter instance. 302 | * 303 | * @return array 304 | */ 305 | protected function resolveCallbackParameter(): array 306 | { 307 | return [$this, false]; 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/Contracts/DataTable.php: -------------------------------------------------------------------------------- 1 | |\Illuminate\Support\Collection 14 | */ 15 | public function results(): Collection; 16 | 17 | /** 18 | * Count results. 19 | */ 20 | public function count(): int; 21 | 22 | /** 23 | * Count total items. 24 | */ 25 | public function totalCount(): int; 26 | 27 | /** 28 | * Set auto filter off and run your own filter. 29 | * Overrides global search. 30 | * 31 | * @return static 32 | */ 33 | public function filter(callable $callback, bool $globalSearch = false): self; 34 | 35 | /** 36 | * Perform global search. 37 | */ 38 | public function filtering(): void; 39 | 40 | /** 41 | * Perform column search. 42 | */ 43 | public function columnSearch(): void; 44 | 45 | /** 46 | * Perform pagination. 47 | */ 48 | public function paging(): void; 49 | 50 | /** 51 | * Perform sorting of columns. 52 | */ 53 | public function ordering(): void; 54 | 55 | /** 56 | * Organizes works. 57 | */ 58 | public function make(bool $mDataSupport = true): JsonResponse; 59 | } 60 | -------------------------------------------------------------------------------- /src/Contracts/Formatter.php: -------------------------------------------------------------------------------- 1 | false, 46 | 'ignore_getters' => false, 47 | 'append' => [], 48 | 'edit' => [], 49 | 'filter' => [], 50 | 'order' => [], 51 | 'only' => null, 52 | 'hidden' => [], 53 | 'visible' => [], 54 | ]; 55 | 56 | /** 57 | * Extra/Added columns. 58 | */ 59 | protected array $extraColumns = []; 60 | 61 | /** 62 | * Total records. 63 | */ 64 | protected ?int $totalRecords = null; 65 | 66 | /** 67 | * Total filtered records. 68 | */ 69 | protected ?int $filteredRecords = null; 70 | 71 | /** 72 | * Flag to check if the total records count should be skipped. 73 | */ 74 | protected bool $skipTotalRecords = false; 75 | 76 | /** 77 | * Auto-filter flag. 78 | */ 79 | protected bool $autoFilter = true; 80 | 81 | /** 82 | * Callback to override global search. 83 | * 84 | * @var callable 85 | */ 86 | protected $filterCallback = null; 87 | 88 | /** 89 | * DT row templates container. 90 | */ 91 | protected array $templates = [ 92 | 'DT_RowId' => '', 93 | 'DT_RowClass' => '', 94 | 'DT_RowData' => [], 95 | 'DT_RowAttr' => [], 96 | ]; 97 | 98 | /** 99 | * Custom ordering callback. 100 | * 101 | * @var callable|null 102 | */ 103 | protected $orderCallback = null; 104 | 105 | /** 106 | * Skip pagination as needed. 107 | */ 108 | protected bool $skipPaging = false; 109 | 110 | /** 111 | * Array of data to append on json response. 112 | */ 113 | protected array $appends = []; 114 | 115 | protected Utilities\Config $config; 116 | 117 | protected mixed $serializer; 118 | 119 | protected array $searchPanes = []; 120 | 121 | protected mixed $transformer; 122 | 123 | protected bool $editOnlySelectedColumns = false; 124 | 125 | /** 126 | * Can the DataTable engine be created with these parameters. 127 | * 128 | * @return bool 129 | */ 130 | public static function canCreate(mixed $source) 131 | { 132 | return false; 133 | } 134 | 135 | /** 136 | * Factory method, create and return an instance for the DataTable engine. 137 | * 138 | * @return static 139 | */ 140 | public static function create(mixed $source) 141 | { 142 | return new static($source); 143 | } 144 | 145 | /** 146 | * @param string|array $columns 147 | * @param string|callable|\Yajra\DataTables\Contracts\Formatter $formatter 148 | * @return $this 149 | */ 150 | public function formatColumn($columns, $formatter): static 151 | { 152 | if (is_string($formatter) && class_exists($formatter)) { 153 | $formatter = app($formatter); 154 | } 155 | 156 | if ($formatter instanceof Formatter) { 157 | foreach ((array) $columns as $column) { 158 | $this->addColumn($column.'_formatted', $formatter); 159 | } 160 | 161 | return $this; 162 | } 163 | 164 | if (is_callable($formatter)) { 165 | foreach ((array) $columns as $column) { 166 | $this->addColumn( 167 | $column.'_formatted', 168 | fn ($row) => $formatter(data_get($row, $column), $row) 169 | ); 170 | } 171 | 172 | return $this; 173 | } 174 | 175 | foreach ((array) $columns as $column) { 176 | $this->addColumn( 177 | $column.'_formatted', 178 | fn ($row) => data_get($row, $column) 179 | ); 180 | } 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * Add column in collection. 187 | * 188 | * @param string $name 189 | * @param string|callable|Formatter $content 190 | * @param bool|int $order 191 | * @return $this 192 | */ 193 | public function addColumn($name, $content, $order = false): static 194 | { 195 | $this->extraColumns[] = $name; 196 | 197 | $this->columnDef['append'][] = ['name' => $name, 'content' => $content, 'order' => $order]; 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * Add DT row index column on response. 204 | * 205 | * @return $this 206 | */ 207 | public function addIndexColumn(): static 208 | { 209 | $this->columnDef['index'] = true; 210 | 211 | return $this; 212 | } 213 | 214 | /** 215 | * Prevent the getters Mutators to be applied when converting a collection 216 | * of the Models into the final JSON. 217 | * 218 | * @return $this 219 | */ 220 | public function ignoreGetters(): static 221 | { 222 | $this->columnDef['ignore_getters'] = true; 223 | 224 | return $this; 225 | } 226 | 227 | /** 228 | * Edit column's content. 229 | * 230 | * @param string $name 231 | * @param string|callable $content 232 | * @return $this 233 | */ 234 | public function editColumn($name, $content): static 235 | { 236 | if ($this->editOnlySelectedColumns) { 237 | if (! count($this->request->columns()) || in_array($name, Arr::pluck($this->request->columns(), 'name'))) { 238 | $this->columnDef['edit'][] = ['name' => $name, 'content' => $content]; 239 | } 240 | } else { 241 | $this->columnDef['edit'][] = ['name' => $name, 'content' => $content]; 242 | } 243 | 244 | return $this; 245 | } 246 | 247 | /** 248 | * Remove column from collection. 249 | * 250 | * @return $this 251 | */ 252 | public function removeColumn(): static 253 | { 254 | $names = func_get_args(); 255 | $this->columnDef['excess'] = array_merge($this->getColumnsDefinition()['excess'], $names); 256 | 257 | return $this; 258 | } 259 | 260 | /** 261 | * Get columns definition. 262 | */ 263 | protected function getColumnsDefinition(): array 264 | { 265 | $config = (array) $this->config->get('datatables.columns'); 266 | $allowed = ['excess', 'escape', 'raw', 'blacklist', 'whitelist']; 267 | 268 | return array_replace_recursive(Arr::only($config, $allowed), $this->columnDef); 269 | } 270 | 271 | /** 272 | * Get only selected columns in response. 273 | * 274 | * @return $this 275 | */ 276 | public function only(array $columns = []): static 277 | { 278 | $this->columnDef['only'] = $columns; 279 | 280 | return $this; 281 | } 282 | 283 | /** 284 | * Declare columns to escape values. 285 | * 286 | * @param string|array $columns 287 | * @return $this 288 | */ 289 | public function escapeColumns($columns = '*'): static 290 | { 291 | $this->columnDef['escape'] = $columns; 292 | 293 | return $this; 294 | } 295 | 296 | /** 297 | * Add a makeHidden() to the row object. 298 | * 299 | * @return $this 300 | */ 301 | public function makeHidden(array $attributes = []): static 302 | { 303 | $hidden = (array) Arr::get($this->columnDef, 'hidden', []); 304 | $this->columnDef['hidden'] = array_merge_recursive($hidden, $attributes); 305 | 306 | return $this; 307 | } 308 | 309 | /** 310 | * Add a makeVisible() to the row object. 311 | * 312 | * @return $this 313 | */ 314 | public function makeVisible(array $attributes = []): static 315 | { 316 | $visible = (array) Arr::get($this->columnDef, 'visible', []); 317 | $this->columnDef['visible'] = array_merge_recursive($visible, $attributes); 318 | 319 | return $this; 320 | } 321 | 322 | /** 323 | * Set columns that should not be escaped. 324 | * Optionally merge the defaults from config. 325 | * 326 | * @param bool $merge 327 | * @return $this 328 | */ 329 | public function rawColumns(array $columns, $merge = false): static 330 | { 331 | if ($merge) { 332 | /** @var array[] $config */ 333 | $config = $this->config->get('datatables.columns'); 334 | 335 | $this->columnDef['raw'] = array_merge($config['raw'], $columns); 336 | } else { 337 | $this->columnDef['raw'] = $columns; 338 | } 339 | 340 | return $this; 341 | } 342 | 343 | /** 344 | * Sets DT_RowClass template. 345 | * result: . 346 | * 347 | * @param string|callable $content 348 | * @return $this 349 | */ 350 | public function setRowClass($content): static 351 | { 352 | $this->templates['DT_RowClass'] = $content; 353 | 354 | return $this; 355 | } 356 | 357 | /** 358 | * Sets DT_RowId template. 359 | * result: . 360 | * 361 | * @param string|callable $content 362 | * @return $this 363 | */ 364 | public function setRowId($content): static 365 | { 366 | $this->templates['DT_RowId'] = $content; 367 | 368 | return $this; 369 | } 370 | 371 | /** 372 | * Set DT_RowData templates. 373 | * 374 | * @return $this 375 | */ 376 | public function setRowData(array $data): static 377 | { 378 | $this->templates['DT_RowData'] = $data; 379 | 380 | return $this; 381 | } 382 | 383 | /** 384 | * Add DT_RowData template. 385 | * 386 | * @param string $key 387 | * @param string|callable $value 388 | * @return $this 389 | */ 390 | public function addRowData($key, $value): static 391 | { 392 | $this->templates['DT_RowData'][$key] = $value; 393 | 394 | return $this; 395 | } 396 | 397 | /** 398 | * Set DT_RowAttr templates. 399 | * result: . 400 | * 401 | * @return $this 402 | */ 403 | public function setRowAttr(array $data): static 404 | { 405 | $this->templates['DT_RowAttr'] = $data; 406 | 407 | return $this; 408 | } 409 | 410 | /** 411 | * Add DT_RowAttr template. 412 | * 413 | * @param string $key 414 | * @param string|callable $value 415 | * @return $this 416 | */ 417 | public function addRowAttr($key, $value): static 418 | { 419 | $this->templates['DT_RowAttr'][$key] = $value; 420 | 421 | return $this; 422 | } 423 | 424 | /** 425 | * Append data on json response. 426 | * 427 | * @return $this 428 | */ 429 | public function with(mixed $key, mixed $value = ''): static 430 | { 431 | if (is_array($key)) { 432 | $this->appends = $key; 433 | } else { 434 | $this->appends[$key] = value($value); 435 | } 436 | 437 | return $this; 438 | } 439 | 440 | /** 441 | * Add with query callback value on response. 442 | * 443 | * @return $this 444 | */ 445 | public function withQuery(string $key, callable $value): static 446 | { 447 | $this->appends[$key] = $value; 448 | 449 | return $this; 450 | } 451 | 452 | /** 453 | * Override default ordering method with a closure callback. 454 | * 455 | * @return $this 456 | */ 457 | public function order(callable $closure): static 458 | { 459 | $this->orderCallback = $closure; 460 | 461 | return $this; 462 | } 463 | 464 | /** 465 | * Update list of columns that is not allowed for search/sort. 466 | * 467 | * @return $this 468 | */ 469 | public function blacklist(array $blacklist): static 470 | { 471 | $this->columnDef['blacklist'] = $blacklist; 472 | 473 | return $this; 474 | } 475 | 476 | /** 477 | * Update list of columns that is allowed for search/sort. 478 | * 479 | * @return $this 480 | */ 481 | public function whitelist(array|string $whitelist = '*'): static 482 | { 483 | $this->columnDef['whitelist'] = $whitelist; 484 | 485 | return $this; 486 | } 487 | 488 | /** 489 | * Set smart search config at runtime. 490 | * 491 | * @return $this 492 | */ 493 | public function smart(bool $state = true): static 494 | { 495 | $this->config->set('datatables.search.smart', $state); 496 | 497 | return $this; 498 | } 499 | 500 | /** 501 | * Set starts_with search config at runtime. 502 | * 503 | * @return $this 504 | */ 505 | public function startsWithSearch(bool $state = true): static 506 | { 507 | $this->config->set('datatables.search.starts_with', $state); 508 | 509 | return $this; 510 | } 511 | 512 | /** 513 | * Set multi_term search config at runtime. 514 | * 515 | * @return $this 516 | */ 517 | public function setMultiTerm(bool $multiTerm = true): static 518 | { 519 | $this->config->set('datatables.search.multi_term', $multiTerm); 520 | 521 | return $this; 522 | } 523 | 524 | /** 525 | * Set total records manually. 526 | * 527 | * @return $this 528 | */ 529 | public function setTotalRecords(int $total): static 530 | { 531 | $this->totalRecords = $total; 532 | 533 | return $this; 534 | } 535 | 536 | /** 537 | * Skip total records and set the recordsTotal equals to recordsFiltered. 538 | * This will improve the performance by skipping the total count query. 539 | * 540 | * @return $this 541 | */ 542 | public function skipTotalRecords(): static 543 | { 544 | $this->totalRecords = 0; 545 | $this->skipTotalRecords = true; 546 | 547 | return $this; 548 | } 549 | 550 | /** 551 | * Set filtered records manually. 552 | * 553 | * @return $this 554 | */ 555 | public function setFilteredRecords(int $total): static 556 | { 557 | $this->filteredRecords = $total; 558 | 559 | return $this; 560 | } 561 | 562 | /** 563 | * Skip pagination as needed. 564 | * 565 | * @return $this 566 | */ 567 | public function skipPaging(): static 568 | { 569 | $this->skipPaging = true; 570 | 571 | return $this; 572 | } 573 | 574 | /** 575 | * Skip auto filtering as needed. 576 | * 577 | * @return $this 578 | */ 579 | public function skipAutoFilter(): static 580 | { 581 | $this->autoFilter = false; 582 | 583 | return $this; 584 | } 585 | 586 | /** 587 | * Push a new column name to blacklist. 588 | * 589 | * @param string $column 590 | * @return $this 591 | */ 592 | public function pushToBlacklist($column): static 593 | { 594 | if (! $this->isBlacklisted($column)) { 595 | $this->columnDef['blacklist'][] = $column; 596 | } 597 | 598 | return $this; 599 | } 600 | 601 | /** 602 | * Check if column is blacklisted. 603 | * 604 | * @param string $column 605 | */ 606 | protected function isBlacklisted($column): bool 607 | { 608 | $colDef = $this->getColumnsDefinition(); 609 | 610 | if (in_array($column, $colDef['blacklist'])) { 611 | return true; 612 | } 613 | 614 | if ($colDef['whitelist'] === '*' || in_array($column, $colDef['whitelist'])) { 615 | return false; 616 | } 617 | 618 | return true; 619 | } 620 | 621 | /** 622 | * Perform sorting of columns. 623 | */ 624 | public function ordering(): void 625 | { 626 | if ($this->orderCallback) { 627 | call_user_func_array($this->orderCallback, $this->resolveCallbackParameter()); 628 | } else { 629 | $this->defaultOrdering(); 630 | } 631 | } 632 | 633 | /** 634 | * Resolve callback parameter instance. 635 | * 636 | * @return array 637 | */ 638 | abstract protected function resolveCallbackParameter(); 639 | 640 | /** 641 | * Perform default query orderBy clause. 642 | */ 643 | abstract protected function defaultOrdering(): void; 644 | 645 | /** 646 | * Set auto filter off and run your own filter. 647 | * Overrides global search. 648 | * 649 | * @return $this 650 | */ 651 | public function filter(callable $callback, bool $globalSearch = false): self 652 | { 653 | $this->autoFilter = $globalSearch; 654 | $this->filterCallback = $callback; 655 | 656 | return $this; 657 | } 658 | 659 | /** 660 | * Convert the object to its JSON representation. 661 | * 662 | * @param int $options 663 | * @return \Illuminate\Http\JsonResponse 664 | */ 665 | public function toJson($options = 0) 666 | { 667 | if ($options) { 668 | $this->config->set('datatables.json.options', $options); 669 | } 670 | 671 | return $this->make(); 672 | } 673 | 674 | /** 675 | * Add a search pane options on response. 676 | * 677 | * @param string $column 678 | * @return $this 679 | */ 680 | public function searchPane($column, mixed $options, ?callable $builder = null): static 681 | { 682 | $options = value($options); 683 | 684 | if ($options instanceof Arrayable) { 685 | $options = $options->toArray(); 686 | } 687 | 688 | $this->searchPanes[$column]['options'] = $options; 689 | $this->searchPanes[$column]['builder'] = $builder; 690 | 691 | return $this; 692 | } 693 | 694 | /** 695 | * Convert instance to array. 696 | */ 697 | public function toArray(): array 698 | { 699 | return (array) $this->make()->getData(true); 700 | } 701 | 702 | /** 703 | * Count total items. 704 | */ 705 | public function totalCount(): int 706 | { 707 | return $this->totalRecords ??= $this->count(); 708 | } 709 | 710 | public function editOnlySelectedColumns(): static 711 | { 712 | $this->editOnlySelectedColumns = true; 713 | 714 | return $this; 715 | } 716 | 717 | /** 718 | * Perform necessary filters. 719 | */ 720 | protected function filterRecords(): void 721 | { 722 | if ($this->autoFilter && $this->request->isSearchable()) { 723 | $this->filtering(); 724 | } 725 | 726 | if (is_callable($this->filterCallback)) { 727 | call_user_func_array($this->filterCallback, $this->resolveCallbackParameter()); 728 | } 729 | 730 | $this->columnSearch(); 731 | $this->searchPanesSearch(); 732 | $this->filteredCount(); 733 | } 734 | 735 | /** 736 | * Perform global search. 737 | */ 738 | public function filtering(): void 739 | { 740 | $keyword = $this->request->keyword(); 741 | 742 | if ($this->config->isMultiTerm()) { 743 | $this->smartGlobalSearch($keyword); 744 | 745 | return; 746 | } 747 | 748 | $this->globalSearch($keyword); 749 | } 750 | 751 | /** 752 | * Perform multi-term search by splitting keyword into 753 | * individual words and searches for each of them. 754 | * 755 | * @param string $keyword 756 | */ 757 | protected function smartGlobalSearch($keyword): void 758 | { 759 | collect(explode(' ', $keyword)) 760 | ->reject(fn ($keyword) => trim((string) $keyword) === '') 761 | ->each(function ($keyword) { 762 | $this->globalSearch($keyword); 763 | }); 764 | } 765 | 766 | /** 767 | * Perform global search for the given keyword. 768 | */ 769 | abstract protected function globalSearch(string $keyword): void; 770 | 771 | /** 772 | * Perform search using search pane values. 773 | */ 774 | protected function searchPanesSearch(): void 775 | { 776 | // Add support for search pane. 777 | } 778 | 779 | /** 780 | * Count filtered items. 781 | */ 782 | public function filteredCount(): int 783 | { 784 | return $this->filteredRecords ??= $this->count(); 785 | } 786 | 787 | /** 788 | * Apply pagination. 789 | */ 790 | protected function paginate(): void 791 | { 792 | if ($this->request->isPaginationable() && ! $this->skipPaging) { 793 | $this->paging(); 794 | } 795 | } 796 | 797 | /** 798 | * Transform output. 799 | * 800 | * @param iterable $results 801 | * @param array $processed 802 | */ 803 | protected function transform($results, $processed): array 804 | { 805 | if (isset($this->transformer) && class_exists('Yajra\\DataTables\\Transformers\\FractalTransformer')) { 806 | return app('datatables.transformer')->transform( 807 | $results, 808 | $this->transformer, 809 | $this->serializer ?? null 810 | ); 811 | } 812 | 813 | return Helper::transform($processed); 814 | } 815 | 816 | /** 817 | * Get processed data. 818 | * 819 | * @param iterable $results 820 | * @param bool $object 821 | * 822 | * @throws \Exception 823 | */ 824 | protected function processResults($results, $object = false): array 825 | { 826 | $processor = new DataProcessor( 827 | $results, 828 | $this->getColumnsDefinition(), 829 | $this->templates, 830 | $this->request->start() 831 | ); 832 | 833 | return $processor->process($object); 834 | } 835 | 836 | /** 837 | * Render json response. 838 | */ 839 | protected function render(array $data): JsonResponse 840 | { 841 | $output = $this->attachAppends([ 842 | 'draw' => $this->request->draw(), 843 | 'recordsTotal' => $this->totalRecords, 844 | 'recordsFiltered' => $this->filteredRecords ?? 0, 845 | 'data' => $data, 846 | ]); 847 | 848 | if ($this->config->isDebugging()) { 849 | $output = $this->showDebugger($output); 850 | } 851 | 852 | foreach ($this->searchPanes as $column => $searchPane) { 853 | $output['searchPanes']['options'][$column] = $searchPane['options']; 854 | } 855 | 856 | return new JsonResponse( 857 | $output, 858 | 200, 859 | $this->config->jsonHeaders(), 860 | $this->config->jsonOptions() 861 | ); 862 | } 863 | 864 | /** 865 | * Attach custom with meta on response. 866 | */ 867 | protected function attachAppends(array $data): array 868 | { 869 | return array_merge($data, $this->appends); 870 | } 871 | 872 | /** 873 | * Append debug parameters on output. 874 | */ 875 | protected function showDebugger(array $output): array 876 | { 877 | $output['input'] = $this->request->all(); 878 | 879 | return $output; 880 | } 881 | 882 | /** 883 | * Return an error json response. 884 | * 885 | * @throws \Yajra\DataTables\Exceptions\Exception|\Exception 886 | */ 887 | protected function errorResponse(\Exception $exception): JsonResponse 888 | { 889 | /** @var string $error */ 890 | $error = $this->config->get('datatables.error'); 891 | $debug = $this->config->get('app.debug'); 892 | 893 | if ($error === 'throw' || (! $error && ! $debug)) { 894 | throw $exception; 895 | } 896 | 897 | $this->getLogger()->error($exception); 898 | 899 | return new JsonResponse([ 900 | 'draw' => $this->request->draw(), 901 | 'recordsTotal' => $this->totalRecords, 902 | 'recordsFiltered' => 0, 903 | 'data' => [], 904 | 'error' => $error ? __($error) : 'Exception Message:'.PHP_EOL.PHP_EOL.$exception->getMessage(), 905 | ]); 906 | } 907 | 908 | /** 909 | * Get monolog/logger instance. 910 | * 911 | * @return \Psr\Log\LoggerInterface 912 | */ 913 | public function getLogger() 914 | { 915 | $this->logger = $this->logger ?: app(LoggerInterface::class); 916 | 917 | return $this->logger; 918 | } 919 | 920 | /** 921 | * Set monolog/logger instance. 922 | * 923 | * @return $this 924 | */ 925 | public function setLogger(LoggerInterface $logger): static 926 | { 927 | $this->logger = $logger; 928 | 929 | return $this; 930 | } 931 | 932 | /** 933 | * Setup search keyword. 934 | */ 935 | protected function setupKeyword(string $value): string 936 | { 937 | if ($this->config->isSmartSearch()) { 938 | $keyword = '%'.$value.'%'; 939 | if ($this->config->isWildcard()) { 940 | $keyword = Helper::wildcardLikeString($value); 941 | } 942 | 943 | // remove escaping slash added on js script request 944 | return str_replace('\\', '%', $keyword); 945 | } 946 | 947 | return $value; 948 | } 949 | 950 | /** 951 | * Get column name to be used for filtering and sorting. 952 | */ 953 | protected function getColumnName(int $index, bool $wantsAlias = false): ?string 954 | { 955 | $column = $this->request->columnName($index); 956 | 957 | if (is_null($column)) { 958 | return null; 959 | } 960 | 961 | // DataTables is using make(false) 962 | if (is_numeric($column)) { 963 | $column = $this->getColumnNameByIndex($index); 964 | } 965 | 966 | if (Str::contains(Str::upper($column), ' AS ')) { 967 | $column = Helper::extractColumnName($column, $wantsAlias); 968 | } 969 | 970 | return $column; 971 | } 972 | 973 | /** 974 | * Get column name by order column index. 975 | */ 976 | protected function getColumnNameByIndex(int $index): string 977 | { 978 | $name = (isset($this->columns[$index]) && $this->columns[$index] != '*') 979 | ? $this->columns[$index] 980 | : $this->getPrimaryKeyName(); 981 | 982 | return in_array($name, $this->extraColumns, true) ? $this->getPrimaryKeyName() : $name; 983 | } 984 | 985 | /** 986 | * If column name could not be resolved then use primary key. 987 | */ 988 | protected function getPrimaryKeyName(): string 989 | { 990 | return 'id'; 991 | } 992 | } 993 | -------------------------------------------------------------------------------- /src/DataTables.php: -------------------------------------------------------------------------------- 1 | $engine) { 50 | if ($source instanceof $class) { 51 | $callback = [$engines[$engine], 'create']; 52 | 53 | if (is_callable($callback)) { 54 | /** @var \Yajra\DataTables\DataTableAbstract $instance */ 55 | $instance = call_user_func_array($callback, $args); 56 | 57 | return $instance; 58 | } 59 | } 60 | } 61 | 62 | foreach ($engines as $engine) { 63 | $canCreate = [$engine, 'canCreate']; 64 | if (is_callable($canCreate) && call_user_func_array($canCreate, $args)) { 65 | $create = [$engine, 'create']; 66 | 67 | if (is_callable($create)) { 68 | /** @var \Yajra\DataTables\DataTableAbstract $instance */ 69 | $instance = call_user_func_array($create, $args); 70 | 71 | return $instance; 72 | } 73 | } 74 | } 75 | 76 | throw new Exception('No available engine for '.$source::class); 77 | } 78 | 79 | /** 80 | * Get request object. 81 | */ 82 | public function getRequest(): Request 83 | { 84 | return app('datatables.request'); 85 | } 86 | 87 | /** 88 | * Get config instance. 89 | */ 90 | public function getConfig(): Config 91 | { 92 | return app('datatables.config'); 93 | } 94 | 95 | /** 96 | * DataTables using query builder. 97 | * 98 | * @throws \Yajra\DataTables\Exceptions\Exception 99 | */ 100 | public function query(QueryBuilder $builder): QueryDataTable 101 | { 102 | /** @var string $dataTable */ 103 | $dataTable = config('datatables.engines.query'); 104 | 105 | $this->validateDataTable($dataTable, QueryDataTable::class); 106 | 107 | return $dataTable::create($builder); 108 | } 109 | 110 | /** 111 | * DataTables using Eloquent Builder. 112 | * 113 | * @throws \Yajra\DataTables\Exceptions\Exception 114 | */ 115 | public function eloquent(EloquentBuilder $builder): EloquentDataTable 116 | { 117 | /** @var string $dataTable */ 118 | $dataTable = config('datatables.engines.eloquent'); 119 | 120 | $this->validateDataTable($dataTable, EloquentDataTable::class); 121 | 122 | return $dataTable::create($builder); 123 | } 124 | 125 | /** 126 | * DataTables using Collection. 127 | * 128 | * @param \Illuminate\Support\Collection|array $collection 129 | * 130 | * @throws \Yajra\DataTables\Exceptions\Exception 131 | */ 132 | public function collection($collection): CollectionDataTable 133 | { 134 | /** @var string $dataTable */ 135 | $dataTable = config('datatables.engines.collection'); 136 | 137 | $this->validateDataTable($dataTable, CollectionDataTable::class); 138 | 139 | return $dataTable::create($collection); 140 | } 141 | 142 | /** 143 | * DataTables using Collection. 144 | * 145 | * @param \Illuminate\Http\Resources\Json\AnonymousResourceCollection|array $resource 146 | * @return ApiResourceDataTable|DataTableAbstract 147 | */ 148 | public function resource($resource) 149 | { 150 | return ApiResourceDataTable::create($resource); 151 | } 152 | 153 | /** 154 | * @throws \Yajra\DataTables\Exceptions\Exception 155 | */ 156 | public function validateDataTable(string $engine, string $parent): void 157 | { 158 | if (! ($engine == $parent || is_subclass_of($engine, $parent))) { 159 | throw new Exception("The given datatable engine `$engine` is not compatible with `$parent`."); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/DataTablesServiceProvider.php: -------------------------------------------------------------------------------- 1 | isLumen()) { 20 | require_once 'lumen.php'; 21 | } 22 | 23 | $this->setupAssets(); 24 | 25 | $this->app->alias('datatables', DataTables::class); 26 | $this->app->singleton('datatables', fn () => new DataTables); 27 | 28 | $this->app->singleton('datatables.request', fn () => new Request); 29 | 30 | $this->app->singleton('datatables.config', Config::class); 31 | } 32 | 33 | /** 34 | * Boot the instance, add macros for datatable engines. 35 | * 36 | * @return void 37 | */ 38 | public function boot() 39 | { 40 | $engines = (array) config('datatables.engines'); 41 | foreach ($engines as $engine => $class) { 42 | $engine = Str::camel($engine); 43 | 44 | if (! method_exists(DataTables::class, $engine) && ! DataTables::hasMacro($engine)) { 45 | DataTables::macro($engine, function () use ($class) { 46 | $canCreate = [$class, 'canCreate']; 47 | if (is_callable($canCreate) && ! call_user_func_array($canCreate, func_get_args())) { 48 | throw new \InvalidArgumentException; 49 | } 50 | 51 | $create = [$class, 'create']; 52 | if (is_callable($create)) { 53 | return call_user_func_array($create, func_get_args()); 54 | } 55 | }); 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * Setup package assets. 62 | * 63 | * @return void 64 | */ 65 | protected function setupAssets() 66 | { 67 | $this->mergeConfigFrom($config = __DIR__.'/config/datatables.php', 'datatables'); 68 | 69 | if ($this->app->runningInConsole()) { 70 | $this->publishes([$config => config_path('datatables.php')], 'datatables'); 71 | } 72 | } 73 | 74 | /** 75 | * Check if app uses Lumen. 76 | * 77 | * @return bool 78 | */ 79 | protected function isLumen() 80 | { 81 | return Str::contains($this->app->version(), 'Lumen'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/EloquentDataTable.php: -------------------------------------------------------------------------------- 1 | $model->newQuery(), 33 | $model instanceof Relation => $model->getQuery(), 34 | $model instanceof EloquentBuilder => $model, 35 | }; 36 | 37 | parent::__construct($builder->getQuery()); 38 | 39 | $this->query = $builder; 40 | } 41 | 42 | /** 43 | * Can the DataTable engine be created with these parameters. 44 | * 45 | * @param mixed $source 46 | */ 47 | public static function canCreate($source): bool 48 | { 49 | return $source instanceof EloquentBuilder; 50 | } 51 | 52 | /** 53 | * Add columns in collection. 54 | * 55 | * @param bool|int $order 56 | * @return $this 57 | */ 58 | public function addColumns(array $names, $order = false) 59 | { 60 | foreach ($names as $name => $attribute) { 61 | if (is_int($name)) { 62 | $name = $attribute; 63 | } 64 | 65 | $this->addColumn($name, fn ($model) => $model->getAttribute($attribute), is_int($order) ? $order++ : $order); 66 | } 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * If column name could not be resolved then use primary key. 73 | */ 74 | protected function getPrimaryKeyName(): string 75 | { 76 | return $this->query->getModel()->getKeyName(); 77 | } 78 | 79 | /** 80 | * {@inheritDoc} 81 | */ 82 | protected function compileQuerySearch($query, string $column, string $keyword, string $boolean = 'or', bool $nested = false): void 83 | { 84 | if (substr_count($column, '.') > 1) { 85 | $parts = explode('.', $column); 86 | $firstRelation = array_shift($parts); 87 | $column = implode('.', $parts); 88 | 89 | if ($this->isMorphRelation($firstRelation)) { 90 | $query->{$boolean.'WhereHasMorph'}( 91 | $firstRelation, 92 | '*', 93 | function (EloquentBuilder $query) use ($column, $keyword) { 94 | parent::compileQuerySearch($query, $column, $keyword, ''); 95 | } 96 | ); 97 | } else { 98 | $query->{$boolean.'WhereHas'}($firstRelation, function (EloquentBuilder $query) use ($column, $keyword) { 99 | self::compileQuerySearch($query, $column, $keyword, '', true); 100 | }); 101 | } 102 | 103 | return; 104 | } 105 | 106 | $parts = explode('.', $column); 107 | $newColumn = array_pop($parts); 108 | $relation = implode('.', $parts); 109 | 110 | if (! $nested && $this->isNotEagerLoaded($relation)) { 111 | parent::compileQuerySearch($query, $column, $keyword, $boolean); 112 | 113 | return; 114 | } 115 | 116 | if ($this->isMorphRelation($relation)) { 117 | $query->{$boolean.'WhereHasMorph'}( 118 | $relation, 119 | '*', 120 | function (EloquentBuilder $query) use ($newColumn, $keyword) { 121 | parent::compileQuerySearch($query, $newColumn, $keyword, ''); 122 | } 123 | ); 124 | } else { 125 | $query->{$boolean.'WhereHas'}($relation, function (EloquentBuilder $query) use ($newColumn, $keyword) { 126 | parent::compileQuerySearch($query, $newColumn, $keyword, ''); 127 | }); 128 | } 129 | } 130 | 131 | /** 132 | * Check if a relation was not used on eager loading. 133 | * 134 | * @param string $relation 135 | * @return bool 136 | */ 137 | protected function isNotEagerLoaded($relation) 138 | { 139 | return ! $relation 140 | || ! array_key_exists($relation, $this->query->getEagerLoads()) 141 | || $relation === $this->query->getModel()->getTable(); 142 | } 143 | 144 | /** 145 | * Check if a relation is a morphed one or not. 146 | * 147 | * @param string $relation 148 | * @return bool 149 | */ 150 | protected function isMorphRelation($relation) 151 | { 152 | $isMorph = false; 153 | if ($relation !== null && $relation !== '') { 154 | $relationParts = explode('.', $relation); 155 | $firstRelation = array_shift($relationParts); 156 | $model = $this->query->getModel(); 157 | $isMorph = method_exists($model, $firstRelation) && $model->$firstRelation() instanceof MorphTo; 158 | } 159 | 160 | return $isMorph; 161 | } 162 | 163 | /** 164 | * {@inheritDoc} 165 | * 166 | * @throws \Yajra\DataTables\Exceptions\Exception 167 | */ 168 | protected function resolveRelationColumn(string $column): string 169 | { 170 | $parts = explode('.', $column); 171 | $columnName = array_pop($parts); 172 | $relation = str_replace('[]', '', implode('.', $parts)); 173 | 174 | if ($this->isNotEagerLoaded($relation)) { 175 | return parent::resolveRelationColumn($column); 176 | } 177 | 178 | return $this->joinEagerLoadedColumn($relation, $columnName); 179 | } 180 | 181 | /** 182 | * Join eager loaded relation and get the related column name. 183 | * 184 | * @param string $relation 185 | * @param string $relationColumn 186 | * @return string 187 | * 188 | * @throws \Yajra\DataTables\Exceptions\Exception 189 | */ 190 | protected function joinEagerLoadedColumn($relation, $relationColumn) 191 | { 192 | $tableAlias = $pivotAlias = ''; 193 | $lastQuery = $this->query; 194 | foreach (explode('.', $relation) as $eachRelation) { 195 | $model = $lastQuery->getRelation($eachRelation); 196 | if ($this->enableEagerJoinAliases) { 197 | $lastAlias = $tableAlias ?: $this->getTablePrefix($lastQuery); 198 | $tableAlias = $tableAlias.'_'.$eachRelation; 199 | $pivotAlias = $tableAlias.'_pivot'; 200 | } else { 201 | $lastAlias = $tableAlias ?: $lastQuery->getModel()->getTable(); 202 | } 203 | switch (true) { 204 | case $model instanceof BelongsToMany: 205 | if ($this->enableEagerJoinAliases) { 206 | $pivot = $model->getTable().' as '.$pivotAlias; 207 | } else { 208 | $pivot = $pivotAlias = $model->getTable(); 209 | } 210 | $pivotPK = $pivotAlias.'.'.$model->getForeignPivotKeyName(); 211 | $pivotFK = ltrim($lastAlias.'.'.$model->getParentKeyName(), '.'); 212 | $this->performJoin($pivot, $pivotPK, $pivotFK); 213 | 214 | $related = $model->getRelated(); 215 | if ($this->enableEagerJoinAliases) { 216 | $table = $related->getTable().' as '.$tableAlias; 217 | } else { 218 | $table = $tableAlias = $related->getTable(); 219 | } 220 | $tablePK = $model->getRelatedPivotKeyName(); 221 | $foreign = $pivotAlias.'.'.$tablePK; 222 | $other = $tableAlias.'.'.$related->getKeyName(); 223 | 224 | $lastQuery->addSelect($tableAlias.'.'.$relationColumn); 225 | 226 | break; 227 | 228 | case $model instanceof HasOneThrough: 229 | if ($this->enableEagerJoinAliases) { 230 | $pivot = explode('.', $model->getQualifiedParentKeyName())[0].' as '.$pivotAlias; 231 | } else { 232 | $pivot = $pivotAlias = explode('.', $model->getQualifiedParentKeyName())[0]; 233 | } 234 | $pivotPK = $pivotAlias.'.'.$model->getFirstKeyName(); 235 | $pivotFK = ltrim($lastAlias.'.'.$model->getLocalKeyName(), '.'); 236 | $this->performJoin($pivot, $pivotPK, $pivotFK); 237 | 238 | $related = $model->getRelated(); 239 | if ($this->enableEagerJoinAliases) { 240 | $table = $related->getTable().' as '.$tableAlias; 241 | } else { 242 | $table = $tableAlias = $related->getTable(); 243 | } 244 | $tablePK = $model->getSecondLocalKeyName(); 245 | $foreign = $pivotAlias.'.'.$tablePK; 246 | $other = $tableAlias.'.'.$related->getKeyName(); 247 | 248 | $lastQuery->addSelect($lastQuery->getModel()->getTable().'.*'); 249 | 250 | break; 251 | 252 | case $model instanceof HasOneOrMany: 253 | if ($this->enableEagerJoinAliases) { 254 | $table = $model->getRelated()->getTable().' as '.$tableAlias; 255 | } else { 256 | $table = $tableAlias = $model->getRelated()->getTable(); 257 | } 258 | $foreign = $tableAlias.'.'.$model->getForeignKeyName(); 259 | $other = ltrim($lastAlias.'.'.$model->getLocalKeyName(), '.'); 260 | break; 261 | 262 | case $model instanceof BelongsTo: 263 | if ($this->enableEagerJoinAliases) { 264 | $table = $model->getRelated()->getTable().' as '.$tableAlias; 265 | } else { 266 | $table = $tableAlias = $model->getRelated()->getTable(); 267 | } 268 | $foreign = ltrim($lastAlias.'.'.$model->getForeignKeyName(), '.'); 269 | $other = $tableAlias.'.'.$model->getOwnerKeyName(); 270 | break; 271 | 272 | default: 273 | throw new Exception('Relation '.$model::class.' is not yet supported.'); 274 | } 275 | $this->performJoin($table, $foreign, $other); 276 | $lastQuery = $model->getQuery(); 277 | } 278 | 279 | return $tableAlias.'.'.$relationColumn; 280 | } 281 | 282 | /** 283 | * Enable the generation of unique table aliases on eagerly loaded join columns. 284 | * You may want to enable it if you encounter a "Not unique table/alias" error when performing a search or applying ordering. 285 | * 286 | * @return $this 287 | */ 288 | public function enableEagerJoinAliases(): static 289 | { 290 | $this->enableEagerJoinAliases = true; 291 | 292 | return $this; 293 | } 294 | 295 | /** 296 | * Perform join query. 297 | * 298 | * @param string $table 299 | * @param string $foreign 300 | * @param string $other 301 | * @param string $type 302 | */ 303 | protected function performJoin($table, $foreign, $other, $type = 'left'): void 304 | { 305 | $joins = []; 306 | $builder = $this->getBaseQueryBuilder(); 307 | foreach ($builder->joins ?? [] as $join) { 308 | $joins[] = $join->table; 309 | } 310 | 311 | if (! in_array($table, $joins)) { 312 | $this->getBaseQueryBuilder()->join($table, $foreign, '=', $other, $type); 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/Exceptions/Exception.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | protected array $appendColumns = []; 18 | 19 | /** 20 | * @var array 21 | */ 22 | protected array $editColumns = []; 23 | 24 | protected array $rawColumns = []; 25 | 26 | /** 27 | * @var array|string[] 28 | */ 29 | protected array $exceptions = ['DT_RowId', 'DT_RowClass', 'DT_RowData', 'DT_RowAttr']; 30 | 31 | protected array $onlyColumns = []; 32 | 33 | protected array $makeHidden = []; 34 | 35 | protected array $makeVisible = []; 36 | 37 | protected array $excessColumns = []; 38 | 39 | /** 40 | * @var string|array 41 | */ 42 | protected mixed $escapeColumns = []; 43 | 44 | protected bool $includeIndex = false; 45 | 46 | protected bool $ignoreGetters = false; 47 | 48 | public function __construct(protected iterable $results, array $columnDef, protected array $templates, protected int $start = 0) 49 | { 50 | $this->appendColumns = $columnDef['append'] ?? []; 51 | $this->editColumns = $columnDef['edit'] ?? []; 52 | $this->excessColumns = $columnDef['excess'] ?? []; 53 | $this->onlyColumns = $columnDef['only'] ?? []; 54 | $this->escapeColumns = $columnDef['escape'] ?? []; 55 | $this->includeIndex = $columnDef['index'] ?? false; 56 | $this->rawColumns = $columnDef['raw'] ?? []; 57 | $this->makeHidden = $columnDef['hidden'] ?? []; 58 | $this->makeVisible = $columnDef['visible'] ?? []; 59 | $this->ignoreGetters = $columnDef['ignore_getters'] ?? false; 60 | } 61 | 62 | /** 63 | * Process data to output on browser. 64 | * 65 | * @param bool $object 66 | */ 67 | public function process($object = false): array 68 | { 69 | $this->output = []; 70 | $indexColumn = config('datatables.index_column', 'DT_RowIndex'); 71 | 72 | foreach ($this->results as $row) { 73 | $data = Helper::convertToArray($row, ['hidden' => $this->makeHidden, 'visible' => $this->makeVisible, 'ignore_getters' => $this->ignoreGetters]); 74 | $value = $this->addColumns($data, $row); 75 | $value = $this->editColumns($value, $row); 76 | $value = $this->setupRowVariables($value, $row); 77 | $value = $this->selectOnlyNeededColumns($value); 78 | $value = $this->removeExcessColumns($value); 79 | 80 | if ($this->includeIndex) { 81 | $value[$indexColumn] = ++$this->start; 82 | } 83 | 84 | $this->output[] = $object ? $value : $this->flatten($value); 85 | } 86 | 87 | return $this->escapeColumns($this->output); 88 | } 89 | 90 | /** 91 | * Process add columns. 92 | * 93 | * @param array|object|\Illuminate\Database\Eloquent\Model $row 94 | */ 95 | protected function addColumns(array $data, $row): array 96 | { 97 | foreach ($this->appendColumns as $value) { 98 | $content = $value['content']; 99 | if ($content instanceof Formatter) { 100 | $column = str_replace('_formatted', '', $value['name']); 101 | 102 | $value['content'] = $content->format($data[$column], $row); 103 | if (isset($data[$column])) { 104 | $value['content'] = $content->format($data[$column], $row); 105 | } 106 | } else { 107 | $value['content'] = Helper::compileContent($content, $data, $row); 108 | } 109 | 110 | $data = Helper::includeInArray($value, $data); 111 | } 112 | 113 | return $data; 114 | } 115 | 116 | /** 117 | * Process edit columns. 118 | */ 119 | protected function editColumns(array $data, object|array $row): array 120 | { 121 | foreach ($this->editColumns as $value) { 122 | $value['content'] = Helper::compileContent($value['content'], $data, $row); 123 | Arr::set($data, $value['name'], $value['content']); 124 | } 125 | 126 | return $data; 127 | } 128 | 129 | /** 130 | * Setup additional DT row variables. 131 | */ 132 | protected function setupRowVariables(array $data, object|array $row): array 133 | { 134 | $processor = new RowProcessor($data, $row); 135 | 136 | return $processor 137 | ->rowValue('DT_RowId', $this->templates['DT_RowId']) 138 | ->rowValue('DT_RowClass', $this->templates['DT_RowClass']) 139 | ->rowData('DT_RowData', $this->templates['DT_RowData']) 140 | ->rowData('DT_RowAttr', $this->templates['DT_RowAttr']) 141 | ->getData(); 142 | } 143 | 144 | /** 145 | * Get only needed columns. 146 | */ 147 | protected function selectOnlyNeededColumns(array $data): array 148 | { 149 | if (empty($this->onlyColumns)) { 150 | return $data; 151 | } else { 152 | $results = []; 153 | foreach ($this->onlyColumns as $onlyColumn) { 154 | Arr::set($results, $onlyColumn, Arr::get($data, $onlyColumn)); 155 | } 156 | foreach ($this->exceptions as $exception) { 157 | if ($column = Arr::get($data, $exception)) { 158 | Arr::set($results, $exception, $column); 159 | } 160 | } 161 | 162 | return $results; 163 | } 164 | } 165 | 166 | /** 167 | * Remove declared hidden columns. 168 | */ 169 | protected function removeExcessColumns(array $data): array 170 | { 171 | foreach ($this->excessColumns as $value) { 172 | Arr::forget($data, $value); 173 | } 174 | 175 | return $data; 176 | } 177 | 178 | /** 179 | * Flatten array with exceptions. 180 | */ 181 | public function flatten(array $array): array 182 | { 183 | $return = []; 184 | foreach ($array as $key => $value) { 185 | if (in_array($key, $this->exceptions)) { 186 | $return[$key] = $value; 187 | } else { 188 | $return[] = $value; 189 | } 190 | } 191 | 192 | return $return; 193 | } 194 | 195 | /** 196 | * Escape column values as declared. 197 | */ 198 | protected function escapeColumns(array $output): array 199 | { 200 | return array_map(function ($row) { 201 | if ($this->escapeColumns == '*') { 202 | $row = $this->escapeRow($row); 203 | } elseif (is_array($this->escapeColumns)) { 204 | $columns = array_diff($this->escapeColumns, $this->rawColumns); 205 | foreach ($columns as $key) { 206 | /** @var string $content */ 207 | $content = Arr::get($row, $key); 208 | Arr::set($row, $key, e($content)); 209 | } 210 | } 211 | 212 | return $row; 213 | }, $output); 214 | } 215 | 216 | /** 217 | * Escape all string or Htmlable values of row. 218 | */ 219 | protected function escapeRow(array $row): array 220 | { 221 | $arrayDot = array_filter(Arr::dot($row)); 222 | foreach ($arrayDot as $key => $value) { 223 | if (! in_array($key, $this->rawColumns)) { 224 | $arrayDot[$key] = (is_string($value) || $value instanceof Htmlable) ? e($value) : $value; 225 | } 226 | } 227 | 228 | foreach ($arrayDot as $key => $value) { 229 | Arr::set($row, $key, $value); 230 | } 231 | 232 | return $row; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/Processors/RowProcessor.php: -------------------------------------------------------------------------------- 1 | data, $template)) { 28 | $this->data[$attribute] = Arr::get($this->data, $template); 29 | } else { 30 | $this->data[$attribute] = Helper::compileContent($template, $this->data, $this->row); 31 | } 32 | } 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * Process DT Row Data and Attr. 39 | * 40 | * @param string $attribute 41 | * @return $this 42 | * 43 | * @throws \ReflectionException 44 | */ 45 | public function rowData($attribute, array $template) 46 | { 47 | if (count($template)) { 48 | $this->data[$attribute] = []; 49 | foreach ($template as $key => $value) { 50 | $this->data[$attribute][$key] = Helper::compileContent($value, $this->data, $this->row); 51 | } 52 | } 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * @return array 59 | */ 60 | public function getData() 61 | { 62 | return $this->data; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/QueryDataTable.php: -------------------------------------------------------------------------------- 1 | 87 | */ 88 | protected Collection $results; 89 | 90 | public function __construct(protected QueryBuilder $query) 91 | { 92 | $this->request = app('datatables.request'); 93 | $this->config = app('datatables.config'); 94 | $this->columns = $this->query->getColumns(); 95 | 96 | if ($this->config->isDebugging()) { 97 | $this->getConnection()->enableQueryLog(); 98 | } 99 | } 100 | 101 | public function getConnection(): Connection 102 | { 103 | /** @var Connection $connection */ 104 | $connection = $this->query->getConnection(); 105 | 106 | return $connection; 107 | } 108 | 109 | /** 110 | * Can the DataTable engine be created with these parameters. 111 | * 112 | * @param mixed $source 113 | */ 114 | public static function canCreate($source): bool 115 | { 116 | return $source instanceof QueryBuilder && ! ($source instanceof EloquentBuilder); 117 | } 118 | 119 | /** 120 | * Organizes works. 121 | * 122 | * @throws \Exception 123 | */ 124 | public function make(bool $mDataSupport = true): JsonResponse 125 | { 126 | try { 127 | $results = $this->prepareQuery()->results(); 128 | $processed = $this->processResults($results, $mDataSupport); 129 | $data = $this->transform($results, $processed); 130 | 131 | return $this->render($data); 132 | } catch (\Exception $exception) { 133 | return $this->errorResponse($exception); 134 | } 135 | } 136 | 137 | /** 138 | * Get paginated results. 139 | * 140 | * @return Collection 141 | */ 142 | public function results(): Collection 143 | { 144 | return $this->results ??= $this->query->get(); 145 | } 146 | 147 | /** 148 | * Prepare query by executing count, filter, order and paginate. 149 | * 150 | * @return $this 151 | */ 152 | public function prepareQuery(): static 153 | { 154 | if (! $this->prepared) { 155 | $this->totalRecords = $this->totalCount(); 156 | 157 | $this->filterRecords(); 158 | $this->ordering(); 159 | $this->paginate(); 160 | } 161 | 162 | $this->prepared = true; 163 | 164 | return $this; 165 | } 166 | 167 | /** 168 | * Counts current query. 169 | */ 170 | public function count(): int 171 | { 172 | return $this->prepareCountQuery()->count(); 173 | } 174 | 175 | /** 176 | * Prepare count query builder. 177 | */ 178 | public function prepareCountQuery(): QueryBuilder 179 | { 180 | $builder = clone $this->query; 181 | 182 | if ($this->isComplexQuery($builder)) { 183 | $builder->select(DB::raw('1 as dt_row_count')); 184 | $clone = $builder->clone(); 185 | $clone->setBindings([]); 186 | if ($clone instanceof EloquentBuilder) { 187 | $clone->getQuery()->wheres = []; 188 | } else { 189 | $clone->wheres = []; 190 | } 191 | 192 | if ($this->isComplexQuery($clone)) { 193 | if (! $this->ignoreSelectInCountQuery) { 194 | $builder = clone $this->query; 195 | } 196 | 197 | return $this->getConnection() 198 | ->query() 199 | ->fromRaw('('.$builder->toSql().') count_row_table') 200 | ->setBindings($builder->getBindings()); 201 | } 202 | } 203 | $row_count = $this->wrap('row_count'); 204 | $builder->select($this->getConnection()->raw("'1' as {$row_count}")); 205 | 206 | if (! $this->keepSelectBindings) { 207 | $builder->setBindings([], 'select'); 208 | } 209 | 210 | return $builder; 211 | } 212 | 213 | /** 214 | * Check if builder query uses complex sql. 215 | * 216 | * @param QueryBuilder|EloquentBuilder $query 217 | */ 218 | protected function isComplexQuery($query): bool 219 | { 220 | return Str::contains(Str::lower($query->toSql()), ['union', 'having', 'distinct', 'order by', 'group by']); 221 | } 222 | 223 | /** 224 | * Wrap column with DB grammar. 225 | */ 226 | protected function wrap(string $column): string 227 | { 228 | return $this->getConnection()->getQueryGrammar()->wrap($column); 229 | } 230 | 231 | /** 232 | * Keep the select bindings. 233 | * 234 | * @return $this 235 | */ 236 | public function keepSelectBindings(): static 237 | { 238 | $this->keepSelectBindings = true; 239 | 240 | return $this; 241 | } 242 | 243 | /** 244 | * Perform column search. 245 | */ 246 | protected function filterRecords(): void 247 | { 248 | $initialQuery = clone $this->query; 249 | 250 | if ($this->autoFilter && $this->request->isSearchable()) { 251 | $this->filtering(); 252 | } 253 | 254 | if (is_callable($this->filterCallback)) { 255 | call_user_func_array($this->filterCallback, $this->resolveCallbackParameter()); 256 | } 257 | 258 | $this->columnSearch(); 259 | $this->searchPanesSearch(); 260 | 261 | // If no modification between the original query and the filtered one has been made 262 | // the filteredRecords equals the totalRecords 263 | if (! $this->skipTotalRecords && $this->query == $initialQuery) { 264 | $this->filteredRecords ??= $this->totalRecords; 265 | } else { 266 | $this->filteredCount(); 267 | 268 | if ($this->skipTotalRecords) { 269 | $this->totalRecords = $this->filteredRecords; 270 | } 271 | } 272 | } 273 | 274 | /** 275 | * Perform column search. 276 | */ 277 | public function columnSearch(): void 278 | { 279 | $columns = $this->request->columns(); 280 | 281 | foreach ($columns as $index => $column) { 282 | $column = $this->getColumnName($index); 283 | 284 | if (is_null($column)) { 285 | continue; 286 | } 287 | 288 | if (! $this->request->isColumnSearchable($index) || $this->isBlacklisted($column) && ! $this->hasFilterColumn($column)) { 289 | continue; 290 | } 291 | 292 | if ($this->hasFilterColumn($column)) { 293 | $keyword = $this->getColumnSearchKeyword($index, true); 294 | $this->applyFilterColumn($this->getBaseQueryBuilder(), $column, $keyword); 295 | } else { 296 | $column = $this->resolveRelationColumn($column); 297 | $keyword = $this->getColumnSearchKeyword($index); 298 | $this->compileColumnSearch($index, $column, $keyword); 299 | } 300 | } 301 | } 302 | 303 | /** 304 | * Check if column has custom filter handler. 305 | */ 306 | public function hasFilterColumn(string $columnName): bool 307 | { 308 | return isset($this->columnDef['filter'][$columnName]); 309 | } 310 | 311 | /** 312 | * Get column keyword to use for search. 313 | */ 314 | protected function getColumnSearchKeyword(int $i, bool $raw = false): string 315 | { 316 | $keyword = $this->request->columnKeyword($i); 317 | if ($raw || $this->request->isRegex($i)) { 318 | return $keyword; 319 | } 320 | 321 | return $this->setupKeyword($keyword); 322 | } 323 | 324 | protected function getColumnNameByIndex(int $index): string 325 | { 326 | $name = (isset($this->columns[$index]) && $this->columns[$index] != '*') 327 | ? $this->columns[$index] 328 | : $this->getPrimaryKeyName(); 329 | 330 | if ($name instanceof Expression) { 331 | $name = $name->getValue($this->query->getGrammar()); 332 | } 333 | 334 | return in_array($name, $this->extraColumns, true) ? $this->getPrimaryKeyName() : $name; 335 | } 336 | 337 | /** 338 | * Apply filterColumn api search. 339 | * 340 | * @param QueryBuilder $query 341 | */ 342 | protected function applyFilterColumn($query, string $columnName, string $keyword, string $boolean = 'and'): void 343 | { 344 | $query = $this->getBaseQueryBuilder($query); 345 | $callback = $this->columnDef['filter'][$columnName]['method']; 346 | 347 | if ($this->query instanceof EloquentBuilder) { 348 | $builder = $this->query->newModelInstance()->newQuery(); 349 | } else { 350 | $builder = $this->query->newQuery(); 351 | } 352 | 353 | $callback($builder, $keyword, fn ($column) => $this->resolveRelationColumn($column)); 354 | 355 | /** @var \Illuminate\Database\Query\Builder $baseQueryBuilder */ 356 | $baseQueryBuilder = $this->getBaseQueryBuilder($builder); 357 | $query->addNestedWhereQuery($baseQueryBuilder, $boolean); 358 | } 359 | 360 | /** 361 | * Get the base query builder instance. 362 | * 363 | * @param QueryBuilder|EloquentBuilder|null $instance 364 | */ 365 | protected function getBaseQueryBuilder($instance = null): QueryBuilder 366 | { 367 | if (! $instance) { 368 | $instance = $this->query; 369 | } 370 | 371 | if ($instance instanceof EloquentBuilder) { 372 | return $instance->getQuery(); 373 | } 374 | 375 | return $instance; 376 | } 377 | 378 | /** 379 | * Get query builder instance. 380 | */ 381 | public function getQuery(): QueryBuilder 382 | { 383 | return $this->query; 384 | } 385 | 386 | /** 387 | * Resolve the proper column name to be used. 388 | */ 389 | protected function resolveRelationColumn(string $column): string 390 | { 391 | return $this->addTablePrefix($this->query, $column); 392 | } 393 | 394 | /** 395 | * Compile queries for column search. 396 | */ 397 | protected function compileColumnSearch(int $i, string $column, string $keyword): void 398 | { 399 | if ($this->request->isRegex($i)) { 400 | $this->regexColumnSearch($column, $keyword); 401 | } else { 402 | $this->compileQuerySearch($this->query, $column, $keyword, ''); 403 | } 404 | } 405 | 406 | /** 407 | * Compile regex query column search. 408 | */ 409 | protected function regexColumnSearch(string $column, string $keyword): void 410 | { 411 | $column = $this->wrap($column); 412 | 413 | switch ($this->getConnection()->getDriverName()) { 414 | case 'oracle': 415 | $sql = ! $this->config->isCaseInsensitive() 416 | ? 'REGEXP_LIKE( '.$column.' , ? )' 417 | : 'REGEXP_LIKE( LOWER('.$column.') , ?, \'i\' )'; 418 | break; 419 | 420 | case 'pgsql': 421 | $column = $this->castColumn($column); 422 | $sql = ! $this->config->isCaseInsensitive() ? $column.' ~ ?' : $column.' ~* ? '; 423 | break; 424 | 425 | default: 426 | $sql = ! $this->config->isCaseInsensitive() 427 | ? $column.' REGEXP ?' 428 | : 'LOWER('.$column.') REGEXP ?'; 429 | $keyword = Str::lower($keyword); 430 | } 431 | 432 | $this->query->whereRaw($sql, [$keyword]); 433 | } 434 | 435 | /** 436 | * Wrap a column and cast based on database driver. 437 | */ 438 | protected function castColumn(string $column): string 439 | { 440 | return match ($this->getConnection()->getDriverName()) { 441 | 'pgsql' => 'CAST('.$column.' as TEXT)', 442 | 'firebird' => 'CAST('.$column.' as VARCHAR(255))', 443 | default => $column, 444 | }; 445 | } 446 | 447 | /** 448 | * Compile query builder where clause depending on configurations. 449 | * 450 | * @param QueryBuilder|EloquentBuilder $query 451 | */ 452 | protected function compileQuerySearch($query, string $column, string $keyword, string $boolean = 'or'): void 453 | { 454 | $column = $this->wrap($this->addTablePrefix($query, $column)); 455 | $column = $this->castColumn($column); 456 | $sql = $column.' LIKE ?'; 457 | 458 | if ($this->config->isCaseInsensitive()) { 459 | $sql = 'LOWER('.$column.') LIKE ?'; 460 | } 461 | 462 | $query->{$boolean.'WhereRaw'}($sql, [$this->prepareKeyword($keyword)]); 463 | } 464 | 465 | /** 466 | * Patch for fix about ambiguous field. 467 | * Ambiguous field error will appear when query use join table and search with keyword. 468 | * 469 | * @param QueryBuilder|EloquentBuilder $query 470 | */ 471 | protected function addTablePrefix($query, string $column): string 472 | { 473 | // Column is already prefixed 474 | if (str_contains($column, '.')) { 475 | return $column; 476 | } 477 | 478 | // Extract selected columns from the query 479 | $selects = $this->getSelectedColumns($query); 480 | 481 | // We have a match 482 | if (isset($selects['columns'][$column])) { 483 | return $selects['columns'][$column]; 484 | } 485 | 486 | // Multiple wildcards => Unable to determine prefix 487 | if (in_array('*', $selects['wildcards']) || count(array_unique($selects['wildcards'])) > 1) { 488 | return $column; 489 | } 490 | 491 | // Use the only wildcard available 492 | if (! empty($selects['wildcards'])) { 493 | return $selects['wildcards'][0].'.'.$column; 494 | } 495 | 496 | // Fallback on table prefix 497 | return ltrim($this->getTablePrefix($query).'.'.$column, '.'); 498 | } 499 | 500 | /** 501 | * Try to get the base table prefix. 502 | * To be used to prevent ambiguous field name. 503 | * 504 | * @param QueryBuilder|EloquentBuilder $query 505 | */ 506 | protected function getTablePrefix($query): ?string 507 | { 508 | $q = $this->getBaseQueryBuilder($query); 509 | $from = $q->from ?? ''; 510 | 511 | if (! $from instanceof Expression) { 512 | if (str_contains((string) $from, ' as ')) { 513 | $from = explode(' as ', (string) $from)[1]; 514 | } 515 | 516 | return $from; 517 | } 518 | 519 | return null; 520 | } 521 | 522 | /** 523 | * Get declared column names from the query. 524 | * 525 | * @param QueryBuilder|EloquentBuilder $query 526 | */ 527 | protected function getSelectedColumns($query): array 528 | { 529 | $q = $this->getBaseQueryBuilder($query); 530 | 531 | $selects = [ 532 | 'wildcards' => [], 533 | 'columns' => [], 534 | ]; 535 | 536 | foreach ($q->columns ?? [] as $select) { 537 | $sql = trim($select instanceof Expression ? $select->getValue($this->getConnection()->getQueryGrammar()) : $select); 538 | // Remove expressions 539 | $sql = preg_replace('/\s*\w*\((?:[^()]*|(?R))*\)/', '_', $sql); 540 | // Remove multiple spaces 541 | $sql = preg_replace('/\s+/', ' ', $sql); 542 | // Remove wrappers 543 | $sql = str_replace(['`', '"', '[', ']'], '', $sql); 544 | // Loop on select columns 545 | foreach (explode(',', $sql) as $column) { 546 | $column = trim($column); 547 | if (preg_match('/[\w.]+\s+(?:as\s+)?([a-zA-Z0-9_]+)$/i', $column, $matches)) { 548 | // Column with alias 549 | $selects['columns'][$matches[1]] = $matches[1]; 550 | } elseif (preg_match('/^([\w.]+)$/i', $column)) { 551 | // Column without alias 552 | [$table, $name] = str_contains($column, '.') ? explode('.', $column) : [null, $column]; 553 | if ($name === '*') { 554 | $selects['wildcards'][] = $table ?? '*'; 555 | } else { 556 | $selects['columns'][$name] = $column; 557 | } 558 | } 559 | } 560 | } 561 | 562 | return $selects; 563 | } 564 | 565 | /** 566 | * Prepare search keyword based on configurations. 567 | */ 568 | protected function prepareKeyword(string $keyword): string 569 | { 570 | if ($this->config->isCaseInsensitive()) { 571 | $keyword = Str::lower($keyword); 572 | } 573 | 574 | if ($this->config->isStartsWithSearch()) { 575 | return "$keyword%"; 576 | } 577 | 578 | if ($this->config->isWildcard()) { 579 | $keyword = Helper::wildcardLikeString($keyword); 580 | } 581 | 582 | if ($this->config->isSmartSearch()) { 583 | $keyword = "%$keyword%"; 584 | } 585 | 586 | return $keyword; 587 | } 588 | 589 | /** 590 | * Add custom filter handler for the give column. 591 | * 592 | * @param string $column 593 | * @return $this 594 | */ 595 | public function filterColumn($column, callable $callback): static 596 | { 597 | $this->columnDef['filter'][$column] = ['method' => $callback]; 598 | 599 | return $this; 600 | } 601 | 602 | /** 603 | * Order each given columns versus the given custom sql. 604 | * 605 | * @param string $sql 606 | * @param array $bindings 607 | * @return $this 608 | */ 609 | public function orderColumns(array $columns, $sql, $bindings = []): static 610 | { 611 | foreach ($columns as $column) { 612 | $this->orderColumn($column, str_replace(':column', $column, $sql), $bindings); 613 | } 614 | 615 | return $this; 616 | } 617 | 618 | /** 619 | * Override default column ordering. 620 | * 621 | * @param string $column 622 | * @param string|\Closure $sql 623 | * @param array $bindings 624 | * @return $this 625 | * 626 | * @internal string $1 Special variable that returns the requested order direction of the column. 627 | */ 628 | public function orderColumn($column, $sql, $bindings = []): static 629 | { 630 | $this->columnDef['order'][$column] = compact('sql', 'bindings'); 631 | 632 | return $this; 633 | } 634 | 635 | /** 636 | * Set datatables to do ordering with NULLS LAST option. 637 | * 638 | * @return $this 639 | */ 640 | public function orderByNullsLast(): static 641 | { 642 | $this->nullsLast = true; 643 | 644 | return $this; 645 | } 646 | 647 | /** 648 | * Perform pagination. 649 | */ 650 | public function paging(): void 651 | { 652 | $start = $this->request->start(); 653 | $length = $this->request->length(); 654 | 655 | $limit = $length > 0 ? $length : 10; 656 | 657 | if (is_callable($this->limitCallback)) { 658 | $this->query->limit($limit); 659 | call_user_func_array($this->limitCallback, [$this->query]); 660 | } else { 661 | $this->query->skip($start)->take($limit); 662 | } 663 | } 664 | 665 | /** 666 | * Paginate dataTable using limit without offset 667 | * with additional where clause via callback. 668 | * 669 | * @return $this 670 | */ 671 | public function limit(callable $callback): static 672 | { 673 | $this->limitCallback = $callback; 674 | 675 | return $this; 676 | } 677 | 678 | /** 679 | * Add column in collection. 680 | * 681 | * @param string $name 682 | * @param string|callable $content 683 | * @param bool|int $order 684 | * @return $this 685 | */ 686 | public function addColumn($name, $content, $order = false): static 687 | { 688 | $this->pushToBlacklist($name); 689 | 690 | return parent::addColumn($name, $content, $order); 691 | } 692 | 693 | /** 694 | * Perform search using search pane values. 695 | * 696 | * 697 | * @throws \Psr\Container\ContainerExceptionInterface 698 | * @throws \Psr\Container\NotFoundExceptionInterface 699 | */ 700 | protected function searchPanesSearch(): void 701 | { 702 | /** @var string[] $columns */ 703 | $columns = $this->request->get('searchPanes', []); 704 | 705 | foreach ($columns as $column => $values) { 706 | if ($this->isBlacklisted($column)) { 707 | continue; 708 | } 709 | 710 | if ($this->searchPanes[$column] && $callback = $this->searchPanes[$column]['builder']) { 711 | $callback($this->query, $values); 712 | } else { 713 | $this->query->whereIn($column, $values); 714 | } 715 | } 716 | } 717 | 718 | /** 719 | * Resolve callback parameter instance. 720 | * 721 | * @return array 722 | */ 723 | protected function resolveCallbackParameter(): array 724 | { 725 | return [$this->query, $this->scoutSearched, fn ($column) => $this->resolveRelationColumn($column)]; 726 | } 727 | 728 | /** 729 | * Perform default query orderBy clause. 730 | * 731 | * 732 | * @throws \Psr\Container\ContainerExceptionInterface 733 | * @throws \Psr\Container\NotFoundExceptionInterface 734 | */ 735 | protected function defaultOrdering(): void 736 | { 737 | collect($this->request->orderableColumns()) 738 | ->map(function ($orderable) { 739 | $orderable['name'] = $this->getColumnName($orderable['column'], true); 740 | 741 | return $orderable; 742 | }) 743 | ->reject(fn ($orderable) => $this->isBlacklisted($orderable['name']) && ! $this->hasOrderColumn($orderable['name'])) 744 | ->each(function ($orderable) { 745 | if ($this->hasOrderColumn($orderable['name'])) { 746 | $this->applyOrderColumn($orderable); 747 | } else { 748 | $column = $this->resolveRelationColumn($orderable['name']); 749 | $nullsLastSql = $this->getNullsLastSql($column, $orderable['direction']); 750 | $normalSql = $this->wrap($column).' '.$orderable['direction']; 751 | $sql = $this->nullsLast ? $nullsLastSql : $normalSql; 752 | $this->query->orderByRaw($sql); 753 | } 754 | }); 755 | } 756 | 757 | /** 758 | * Check if column has custom sort handler. 759 | */ 760 | protected function hasOrderColumn(string $column): bool 761 | { 762 | return isset($this->columnDef['order'][$column]); 763 | } 764 | 765 | /** 766 | * Apply orderColumn custom query. 767 | */ 768 | protected function applyOrderColumn(array $orderable): void 769 | { 770 | $sql = $this->columnDef['order'][$orderable['name']]['sql']; 771 | if ($sql === false) { 772 | return; 773 | } 774 | 775 | if (is_callable($sql)) { 776 | call_user_func($sql, $this->query, $orderable['direction'], fn ($column) => $this->resolveRelationColumn($column)); 777 | } else { 778 | $sql = str_replace('$1', $orderable['direction'], (string) $sql); 779 | $bindings = $this->columnDef['order'][$orderable['name']]['bindings']; 780 | $this->query->orderByRaw($sql, $bindings); 781 | } 782 | } 783 | 784 | /** 785 | * Get NULLS LAST SQL. 786 | * 787 | * @param string $column 788 | * @param string $direction 789 | * 790 | * @throws \Psr\Container\ContainerExceptionInterface 791 | * @throws \Psr\Container\NotFoundExceptionInterface 792 | */ 793 | protected function getNullsLastSql($column, $direction): string 794 | { 795 | /** @var string $sql */ 796 | $sql = $this->config->get('datatables.nulls_last_sql', '%s %s NULLS LAST'); 797 | 798 | return str_replace( 799 | [':column', ':direction'], 800 | [$column, $direction], 801 | sprintf($sql, $column, $direction) 802 | ); 803 | } 804 | 805 | /** 806 | * Perform global search for the given keyword. 807 | */ 808 | protected function globalSearch(string $keyword): void 809 | { 810 | // Try scout search first & fall back to default search if disabled/failed 811 | if ($this->applyScoutSearch($keyword)) { 812 | return; 813 | } 814 | 815 | $this->query->where(function ($query) use ($keyword) { 816 | collect($this->request->searchableColumnIndex()) 817 | ->map(fn ($index) => $this->getColumnName($index)) 818 | ->filter() 819 | ->reject(fn ($column) => $this->isBlacklisted($column) && ! $this->hasFilterColumn($column)) 820 | ->each(function ($column) use ($keyword, $query) { 821 | if ($this->hasFilterColumn($column)) { 822 | $this->applyFilterColumn($query, $column, $keyword, 'or'); 823 | } else { 824 | $this->compileQuerySearch($query, $column, $keyword); 825 | } 826 | }); 827 | }); 828 | } 829 | 830 | /** 831 | * Perform multi-term search by splitting keyword into 832 | * individual words and searches for each of them. 833 | * 834 | * @param string $keyword 835 | */ 836 | protected function smartGlobalSearch($keyword): void 837 | { 838 | // Try scout search first & fall back to default search if disabled/failed 839 | if ($this->applyScoutSearch($keyword)) { 840 | return; 841 | } 842 | 843 | parent::smartGlobalSearch($keyword); 844 | } 845 | 846 | /** 847 | * Append debug parameters on output. 848 | */ 849 | protected function showDebugger(array $output): array 850 | { 851 | $query_log = $this->getConnection()->getQueryLog(); 852 | array_walk_recursive($query_log, function (&$item) { 853 | if (is_string($item) && extension_loaded('iconv')) { 854 | $item = iconv('iso-8859-1', 'utf-8', $item); 855 | } 856 | }); 857 | 858 | $output['queries'] = $query_log; 859 | $output['input'] = $this->request->all(); 860 | 861 | return $output; 862 | } 863 | 864 | /** 865 | * Attach custom with meta on response. 866 | */ 867 | protected function attachAppends(array $data): array 868 | { 869 | $appends = []; 870 | foreach ($this->appends as $key => $value) { 871 | if (is_callable($value)) { 872 | $appends[$key] = value($value($this->getFilteredQuery())); 873 | } else { 874 | $appends[$key] = $value; 875 | } 876 | } 877 | 878 | // Set flag to disable ordering 879 | $appends['disableOrdering'] = $this->disableUserOrdering; 880 | 881 | return array_merge($data, $appends); 882 | } 883 | 884 | /** 885 | * Get filtered, ordered and paginated query. 886 | */ 887 | public function getFilteredQuery(): QueryBuilder 888 | { 889 | $this->prepareQuery(); 890 | 891 | return $this->getQuery(); 892 | } 893 | 894 | /** 895 | * Ignore the selects in count query. 896 | * 897 | * @return $this 898 | */ 899 | public function ignoreSelectsInCountQuery(): static 900 | { 901 | $this->ignoreSelectInCountQuery = true; 902 | 903 | return $this; 904 | } 905 | 906 | /** 907 | * Perform sorting of columns. 908 | */ 909 | public function ordering(): void 910 | { 911 | // Skip if user ordering is disabled (e.g. scout search) 912 | if ($this->disableUserOrdering) { 913 | return; 914 | } 915 | 916 | parent::ordering(); 917 | } 918 | 919 | /** 920 | * Enable scout search and use provided model for searching. 921 | * $max_hits is the maximum number of hits to return from scout. 922 | * 923 | * @return $this 924 | * 925 | * @throws \Exception 926 | */ 927 | public function enableScoutSearch(string $model, int $max_hits = 1000): static 928 | { 929 | $scout_model = new $model; 930 | if (! class_exists($model) || ! ($scout_model instanceof Model)) { 931 | throw new \Exception("$model must be an Eloquent Model."); 932 | } 933 | if (! method_exists($scout_model, 'searchableAs') || ! method_exists($scout_model, 'getScoutKeyName')) { 934 | throw new \Exception("$model must use the Searchable trait."); 935 | } 936 | 937 | $this->scoutModel = $scout_model; 938 | $this->scoutMaxHits = $max_hits; 939 | $this->scoutIndex = $this->scoutModel->searchableAs(); 940 | $this->scoutKey = $this->scoutModel->getScoutKeyName(); 941 | 942 | return $this; 943 | } 944 | 945 | /** 946 | * Add dynamic filters to scout search. 947 | * 948 | * @return $this 949 | */ 950 | public function scoutFilter(callable $callback): static 951 | { 952 | $this->scoutFilterCallback = $callback; 953 | 954 | return $this; 955 | } 956 | 957 | /** 958 | * Apply scout search to query if enabled. 959 | */ 960 | protected function applyScoutSearch(string $search_keyword): bool 961 | { 962 | if ($this->scoutModel == null) { 963 | return false; 964 | } 965 | 966 | try { 967 | // Perform scout search 968 | $search_filters = ''; 969 | if (is_callable($this->scoutFilterCallback)) { 970 | $search_filters = ($this->scoutFilterCallback)($search_keyword); 971 | } 972 | 973 | $search_results = $this->performScoutSearch($search_keyword, $search_filters); 974 | 975 | // Apply scout search results to query 976 | $this->query->where(function ($query) use ($search_results) { 977 | $this->query->whereIn($this->scoutKey, $search_results); 978 | }); 979 | 980 | // Order by scout search results & disable user ordering (if db driver is supported) 981 | if (count($search_results) > 0 && $this->applyFixedOrderingToQuery($this->scoutKey, $search_results)) { 982 | // Disable user ordering because we already ordered by search relevancy 983 | $this->disableUserOrdering = true; 984 | } 985 | 986 | $this->scoutSearched = true; 987 | 988 | return true; 989 | } catch (\Exception) { 990 | // Scout search failed, fallback to default search 991 | return false; 992 | } 993 | } 994 | 995 | /** 996 | * Apply fixed ordering to query by a fixed set of values depending on database driver (used for scout search). 997 | * 998 | * Currently supported drivers: MySQL 999 | * 1000 | * @return bool 1001 | */ 1002 | protected function applyFixedOrderingToQuery(string $keyName, array $orderedKeys) 1003 | { 1004 | $connection = $this->getConnection(); 1005 | $driverName = $connection->getDriverName(); 1006 | 1007 | // Escape keyName and orderedKeys 1008 | $keyName = $connection->getQueryGrammar()->wrap($keyName); 1009 | $orderedKeys = collect($orderedKeys) 1010 | ->map(fn ($value) => $connection->escape($value)); 1011 | 1012 | switch ($driverName) { 1013 | case 'mariadb': 1014 | case 'mysql': 1015 | $this->query->orderByRaw("FIELD($keyName, ".$orderedKeys->implode(',').')'); 1016 | 1017 | return true; 1018 | 1019 | case 'pgsql': 1020 | case 'oracle': 1021 | $this->query->orderByRaw( 1022 | 'CASE ' 1023 | . 1024 | $orderedKeys 1025 | ->map(fn ($value, $index) => "WHEN $keyName=$value THEN $index") 1026 | ->implode(' ') 1027 | . 1028 | ' END' 1029 | ); 1030 | 1031 | return true; 1032 | 1033 | case 'sqlite': 1034 | case 'sqlsrv': 1035 | $this->query->orderByRaw( 1036 | "CASE $keyName " 1037 | . 1038 | $orderedKeys 1039 | ->map(fn ($value, $index) => "WHEN $value THEN $index") 1040 | ->implode(' ') 1041 | . 1042 | ' END' 1043 | ); 1044 | 1045 | return true; 1046 | 1047 | default: 1048 | return false; 1049 | } 1050 | } 1051 | 1052 | /** 1053 | * Perform a scout search with the configured engine and given parameters. Return matching model IDs. 1054 | * 1055 | * 1056 | * @throws \Exception 1057 | */ 1058 | protected function performScoutSearch(string $searchKeyword, mixed $searchFilters = []): array 1059 | { 1060 | if (! class_exists(\Laravel\Scout\EngineManager::class)) { 1061 | throw new \Exception('Laravel Scout is not installed.'); 1062 | } 1063 | $engine = app(\Laravel\Scout\EngineManager::class)->engine(); 1064 | 1065 | if ($engine instanceof \Laravel\Scout\Engines\MeilisearchEngine) { 1066 | /** @var \Meilisearch\Client $engine */ 1067 | $search_results = $engine 1068 | ->index($this->scoutIndex) 1069 | ->rawSearch($searchKeyword, [ 1070 | 'limit' => $this->scoutMaxHits, 1071 | 'attributesToRetrieve' => [$this->scoutKey], 1072 | 'filter' => $searchFilters, 1073 | ]); 1074 | 1075 | /** @var array> $hits */ 1076 | $hits = $search_results['hits'] ?? []; 1077 | 1078 | return collect($hits) 1079 | ->pluck($this->scoutKey) 1080 | ->all(); 1081 | } elseif ($engine instanceof \Laravel\Scout\Engines\AlgoliaEngine) { 1082 | /** @var \Algolia\AlgoliaSearch\SearchClient $engine */ 1083 | $algolia = $engine->initIndex($this->scoutIndex); 1084 | 1085 | $search_results = $algolia->search($searchKeyword, [ 1086 | 'offset' => 0, 1087 | 'length' => $this->scoutMaxHits, 1088 | 'attributesToRetrieve' => [$this->scoutKey], 1089 | 'attributesToHighlight' => [], 1090 | 'filters' => $searchFilters, 1091 | ]); 1092 | 1093 | /** @var array> $hits */ 1094 | $hits = $search_results['hits'] ?? []; 1095 | 1096 | return collect($hits) 1097 | ->pluck($this->scoutKey) 1098 | ->all(); 1099 | } else { 1100 | throw new \Exception('Unsupported Scout Engine. Currently supported: Meilisearch, Algolia'); 1101 | } 1102 | } 1103 | } 1104 | -------------------------------------------------------------------------------- /src/Utilities/Config.php: -------------------------------------------------------------------------------- 1 | repository->get('datatables.search.use_wildcards', false); 20 | } 21 | 22 | /** 23 | * Check if config uses smart search. 24 | */ 25 | public function isSmartSearch(): bool 26 | { 27 | return (bool) $this->repository->get('datatables.search.smart', true); 28 | } 29 | 30 | /** 31 | * Check if config uses case-insensitive search. 32 | */ 33 | public function isCaseInsensitive(): bool 34 | { 35 | return (bool) $this->repository->get('datatables.search.case_insensitive', false); 36 | } 37 | 38 | /** 39 | * Check if app is in debug mode. 40 | */ 41 | public function isDebugging(): bool 42 | { 43 | return (bool) $this->repository->get('app.debug', false); 44 | } 45 | 46 | /** 47 | * Get the specified configuration value. 48 | * 49 | * @param string $key 50 | * @return mixed 51 | */ 52 | public function get($key, mixed $default = null) 53 | { 54 | return $this->repository->get($key, $default); 55 | } 56 | 57 | /** 58 | * Set a given configuration value. 59 | * 60 | * @param array|string $key 61 | * @return void 62 | */ 63 | public function set($key, mixed $value = null) 64 | { 65 | $this->repository->set($key, $value); 66 | } 67 | 68 | /** 69 | * Check if dataTable config uses multi-term searching. 70 | */ 71 | public function isMultiTerm(): bool 72 | { 73 | return (bool) $this->repository->get('datatables.search.multi_term', true); 74 | } 75 | 76 | /** 77 | * Check if dataTable config uses starts_with searching. 78 | */ 79 | public function isStartsWithSearch(): bool 80 | { 81 | return (bool) $this->repository->get('datatables.search.starts_with', false); 82 | } 83 | 84 | public function jsonOptions(): int 85 | { 86 | /** @var int $options */ 87 | $options = $this->repository->get('datatables.json.options', 0); 88 | 89 | return $options; 90 | } 91 | 92 | public function jsonHeaders(): array 93 | { 94 | return (array) $this->repository->get('datatables.json.header', []); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Utilities/Helper.php: -------------------------------------------------------------------------------- 1 | $item['content']]); 22 | } 23 | 24 | $count = 0; 25 | $last = $array; 26 | $first = []; 27 | foreach ($array as $key => $value) { 28 | if ($count == $item['order']) { 29 | continue; 30 | } 31 | 32 | unset($last[$key]); 33 | $first[$key] = $value; 34 | 35 | $count++; 36 | } 37 | 38 | return array_merge($first, [$item['name'] => $item['content']], $last); 39 | } 40 | 41 | /** 42 | * Check if item order is valid. 43 | */ 44 | protected static function isItemOrderInvalid(array $item, array $array): bool 45 | { 46 | return $item['order'] === false || $item['order'] >= count($array); 47 | } 48 | 49 | /** 50 | * Gets the parameter of a callable thing (from is_callable) and returns it's arguments using reflection. 51 | * 52 | * @param callable $callable 53 | * @return \ReflectionParameter[] 54 | * 55 | * @throws \ReflectionException 56 | * @throws \InvalidArgumentException 57 | */ 58 | private static function reflectCallableParameters($callable) 59 | { 60 | /* 61 | loosely after https://github.com/technically-php/callable-reflection/blob/main/src/CallableReflection.php#L72-L86. 62 | Licence is compatible, both project use MIT 63 | */ 64 | if ($callable instanceof Closure) { 65 | $reflection = new ReflectionFunction($callable); 66 | } elseif (is_string($callable) && function_exists($callable)) { 67 | $reflection = new ReflectionFunction($callable); 68 | } elseif (is_string($callable) && str_contains($callable, '::')) { 69 | $reflection = new ReflectionMethod($callable); 70 | } elseif (is_object($callable) && method_exists($callable, '__invoke')) { 71 | $reflection = new ReflectionMethod($callable, '__invoke'); 72 | } else { 73 | throw new \InvalidArgumentException('argument is not callable or the code is wrong'); 74 | } 75 | 76 | return $reflection->getParameters(); 77 | } 78 | 79 | /** 80 | * Determines if content is callable or blade string, processes and returns. 81 | * 82 | * @param mixed $content Pre-processed content 83 | * @param array $data data to use with blade template 84 | * @param array|object $param parameter to call with callable 85 | * @return mixed 86 | * 87 | * @throws \ReflectionException 88 | */ 89 | public static function compileContent(mixed $content, array $data, array|object $param) 90 | { 91 | if (is_string($content)) { 92 | return static::compileBlade($content, static::getMixedValue($data, $param)); 93 | } 94 | 95 | if (is_callable($content)) { 96 | $arguments = self::reflectCallableParameters($content); 97 | 98 | if (count($arguments) > 0) { 99 | return app()->call($content, [$arguments[0]->name => $param]); 100 | } 101 | 102 | return $content($param); 103 | } 104 | 105 | if (is_array($content)) { 106 | [$view, $viewData] = $content; 107 | 108 | return static::compileBlade($view, static::getMixedValue($data, $param) + $viewData); 109 | } 110 | 111 | return $content; 112 | } 113 | 114 | /** 115 | * Parses and compiles strings by using Blade Template System. 116 | * 117 | * 118 | * @throws \Throwable 119 | */ 120 | public static function compileBlade(string $str, array $data = []): false|string 121 | { 122 | if (view()->exists($str)) { 123 | /** @var view-string $str */ 124 | return view($str, $data)->render(); 125 | } 126 | 127 | ob_start() && extract($data, EXTR_SKIP); 128 | eval('?>'.app('blade.compiler')->compileString($str)); 129 | $str = ob_get_contents(); 130 | ob_end_clean(); 131 | 132 | return $str; 133 | } 134 | 135 | /** 136 | * Get a mixed value of custom data and the parameters. 137 | */ 138 | public static function getMixedValue(array $data, array|object $param): array 139 | { 140 | $casted = self::castToArray($param); 141 | 142 | $data['model'] = $param; 143 | 144 | foreach ($data as $key => $value) { 145 | if (isset($casted[$key])) { 146 | $data[$key] = $casted[$key]; 147 | } 148 | } 149 | 150 | return $data; 151 | } 152 | 153 | /** 154 | * Cast the parameter into an array. 155 | */ 156 | public static function castToArray(array|object $param): array 157 | { 158 | if ($param instanceof Arrayable) { 159 | return $param->toArray(); 160 | } 161 | 162 | return (array) $param; 163 | } 164 | 165 | /** 166 | * Get equivalent or method of query builder. 167 | */ 168 | public static function getOrMethod(string $method): string 169 | { 170 | if (! Str::contains(Str::lower($method), 'or')) { 171 | return 'or'.ucfirst($method); 172 | } 173 | 174 | return $method; 175 | } 176 | 177 | /** 178 | * Converts array object values to associative array. 179 | */ 180 | public static function convertToArray(mixed $row, array $filters = []): array 181 | { 182 | if (Arr::get($filters, 'ignore_getters') && is_object($row) && method_exists($row, 'getAttributes')) { 183 | $data = $row->getAttributes(); 184 | if (method_exists($row, 'getRelations')) { 185 | foreach ($row->getRelations() as $relationName => $relation) { 186 | if (is_iterable($relation)) { 187 | foreach ($relation as $relationItem) { 188 | $data[$relationName][] = self::convertToArray($relationItem, ['ignore_getters' => true]); 189 | } 190 | } else { 191 | $data[$relationName] = self::convertToArray($relation, ['ignore_getters' => true]); 192 | } 193 | } 194 | } 195 | 196 | return $data; 197 | } 198 | 199 | $row = is_object($row) && method_exists($row, 'makeHidden') ? $row->makeHidden(Arr::get($filters, 'hidden', 200 | [])) : $row; 201 | $row = is_object($row) && method_exists($row, 'makeVisible') ? $row->makeVisible(Arr::get($filters, 'visible', 202 | [])) : $row; 203 | 204 | $data = $row instanceof Arrayable ? $row->toArray() : (array) $row; 205 | foreach ($data as &$value) { 206 | if ((is_object($value) && ! $value instanceof DateTime) || is_array($value)) { 207 | $value = self::convertToArray($value); 208 | } 209 | 210 | unset($value); 211 | } 212 | 213 | return $data; 214 | } 215 | 216 | public static function transform(array $data): array 217 | { 218 | return array_map(fn ($row) => self::transformRow($row), $data); 219 | } 220 | 221 | /** 222 | * Transform row data into an array. 223 | * 224 | * @param array $row 225 | */ 226 | protected static function transformRow($row): array 227 | { 228 | foreach ($row as $key => $value) { 229 | if ($value instanceof DateTime) { 230 | $row[$key] = $value->format('Y-m-d H:i:s'); 231 | } else { 232 | if (is_object($value) && method_exists($value, '__toString')) { 233 | $row[$key] = $value->__toString(); 234 | } else { 235 | $row[$key] = $value; 236 | } 237 | } 238 | } 239 | 240 | return $row; 241 | } 242 | 243 | /** 244 | * Build parameters depending on # of arguments passed. 245 | */ 246 | public static function buildParameters(array $args): array 247 | { 248 | $parameters = []; 249 | 250 | if (count($args) > 2) { 251 | $parameters[] = $args[0]; 252 | foreach ($args[1] as $param) { 253 | $parameters[] = $param; 254 | } 255 | } else { 256 | foreach ($args[0] as $param) { 257 | $parameters[] = $param; 258 | } 259 | } 260 | 261 | return $parameters; 262 | } 263 | 264 | /** 265 | * Replace all pattern occurrences with keyword. 266 | */ 267 | public static function replacePatternWithKeyword(array $subject, string $keyword, string $pattern = '$1'): array 268 | { 269 | $parameters = []; 270 | foreach ($subject as $param) { 271 | if (is_array($param)) { 272 | $parameters[] = self::replacePatternWithKeyword($param, $keyword, $pattern); 273 | } else { 274 | $parameters[] = str_replace($pattern, $keyword, (string) $param); 275 | } 276 | } 277 | 278 | return $parameters; 279 | } 280 | 281 | /** 282 | * Get column name from string. 283 | */ 284 | public static function extractColumnName(string $str, bool $wantsAlias): string 285 | { 286 | $matches = explode(' as ', Str::lower($str)); 287 | 288 | if (count($matches) > 1) { 289 | if ($wantsAlias) { 290 | return array_pop($matches); 291 | } 292 | 293 | return array_shift($matches); 294 | } elseif (strpos($str, '.')) { 295 | $array = explode('.', $str); 296 | 297 | return array_pop($array); 298 | } 299 | 300 | return $str; 301 | } 302 | 303 | /** 304 | * Adds % wildcards to the given string. 305 | */ 306 | public static function wildcardLikeString(string $str, bool $lowercase = true): string 307 | { 308 | return static::wildcardString($str, '%', $lowercase); 309 | } 310 | 311 | /** 312 | * Adds wildcards to the given string. 313 | */ 314 | public static function wildcardString(string $str, string $wildcard, bool $lowercase = true): string 315 | { 316 | $wild = $wildcard; 317 | $chars = (array) preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY); 318 | 319 | if (count($chars) > 0) { 320 | foreach ($chars as $char) { 321 | $wild .= $char.$wildcard; 322 | } 323 | } 324 | 325 | if ($lowercase) { 326 | $wild = Str::lower($wild); 327 | } 328 | 329 | return $wild; 330 | } 331 | 332 | public static function toJsonScript(array $parameters, int $options = 0): string 333 | { 334 | $values = []; 335 | $replacements = []; 336 | 337 | foreach (Arr::dot($parameters) as $key => $value) { 338 | if (self::isJavascript($value, $key)) { 339 | $values[] = trim((string) $value); 340 | Arr::set($parameters, $key, '%'.$key.'%'); 341 | $replacements[] = '"%'.$key.'%"'; 342 | } 343 | } 344 | 345 | $new = []; 346 | foreach ($parameters as $key => $value) { 347 | Arr::set($new, $key, $value); 348 | } 349 | 350 | $json = (string) json_encode($new, $options); 351 | 352 | return str_replace($replacements, $values, $json); 353 | } 354 | 355 | public static function isJavascript(string|array|object|null $value, string $key): bool 356 | { 357 | if (empty($value) || is_array($value) || is_object($value)) { 358 | return false; 359 | } 360 | 361 | /** @var array $callbacks */ 362 | $callbacks = config('datatables.callback', ['$', '$.', 'function']); 363 | 364 | if (Str::startsWith($key, 'language.')) { 365 | return false; 366 | } 367 | 368 | return Str::startsWith(trim($value), $callbacks) || Str::contains($key, ['editor', 'minDate', 'maxDate']); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/Utilities/Request.php: -------------------------------------------------------------------------------- 1 | request = app('request'); 20 | } 21 | 22 | /** 23 | * Proxy non-existing method calls to base request class. 24 | * 25 | * @param string $name 26 | * @param array $arguments 27 | * @return mixed 28 | */ 29 | public function __call($name, $arguments) 30 | { 31 | $callback = [$this->request, $name]; 32 | if (is_callable($callback)) { 33 | return call_user_func_array($callback, $arguments); 34 | } 35 | } 36 | 37 | /** 38 | * Get attributes from request instance. 39 | * 40 | * @param string $name 41 | * @return mixed 42 | */ 43 | public function __get($name) 44 | { 45 | return $this->request->__get($name); 46 | } 47 | 48 | /** 49 | * Get all columns request input. 50 | */ 51 | public function columns(): array 52 | { 53 | return (array) $this->request->input('columns'); 54 | } 55 | 56 | /** 57 | * Check if DataTables is searchable. 58 | */ 59 | public function isSearchable(): bool 60 | { 61 | return $this->request->input('search.value') != ''; 62 | } 63 | 64 | /** 65 | * Check if DataTables must uses regular expressions. 66 | */ 67 | public function isRegex(int $index): bool 68 | { 69 | return $this->request->input("columns.$index.search.regex") === 'true'; 70 | } 71 | 72 | /** 73 | * Get orderable columns. 74 | */ 75 | public function orderableColumns(): array 76 | { 77 | if (! $this->isOrderable()) { 78 | return []; 79 | } 80 | 81 | $orderable = []; 82 | for ($i = 0, $c = count((array) $this->request->input('order')); $i < $c; $i++) { 83 | /** @var int $order_col */ 84 | $order_col = $this->request->input("order.$i.column"); 85 | 86 | /** @var string $direction */ 87 | $direction = $this->request->input("order.$i.dir"); 88 | 89 | $order_dir = $direction && strtolower($direction) === 'asc' ? 'asc' : 'desc'; 90 | if ($this->isColumnOrderable($order_col)) { 91 | $orderable[] = ['column' => $order_col, 'direction' => $order_dir]; 92 | } 93 | } 94 | 95 | return $orderable; 96 | } 97 | 98 | /** 99 | * Check if DataTables ordering is enabled. 100 | */ 101 | public function isOrderable(): bool 102 | { 103 | return $this->request->input('order') && count((array) $this->request->input('order')) > 0; 104 | } 105 | 106 | /** 107 | * Check if a column is orderable. 108 | */ 109 | public function isColumnOrderable(int $index): bool 110 | { 111 | return $this->request->input("columns.$index.orderable", 'true') == 'true'; 112 | } 113 | 114 | /** 115 | * Get searchable column indexes. 116 | * 117 | * @return array 118 | */ 119 | public function searchableColumnIndex() 120 | { 121 | $searchable = []; 122 | $columns = (array) $this->request->input('columns'); 123 | for ($i = 0, $c = count($columns); $i < $c; $i++) { 124 | if ($this->isColumnSearchable($i, false)) { 125 | $searchable[] = $i; 126 | } 127 | } 128 | 129 | return $searchable; 130 | } 131 | 132 | /** 133 | * Check if a column is searchable. 134 | */ 135 | public function isColumnSearchable(int $i, bool $column_search = true): bool 136 | { 137 | if ($column_search) { 138 | return 139 | ( 140 | $this->request->input("columns.$i.searchable", 'true') === 'true' 141 | || 142 | $this->request->input("columns.$i.searchable", 'true') === true 143 | ) 144 | && $this->columnKeyword($i) != ''; 145 | } 146 | 147 | return 148 | $this->request->input("columns.$i.searchable", 'true') === 'true' 149 | || 150 | $this->request->input("columns.$i.searchable", 'true') === true; 151 | } 152 | 153 | /** 154 | * Get column's search value. 155 | */ 156 | public function columnKeyword(int $index): string 157 | { 158 | /** @var string $keyword */ 159 | $keyword = $this->request->input("columns.$index.search.value") ?? ''; 160 | 161 | return $this->prepareKeyword($keyword); 162 | } 163 | 164 | /** 165 | * Prepare keyword string value. 166 | */ 167 | protected function prepareKeyword(float|array|int|string $keyword): string 168 | { 169 | if (is_array($keyword)) { 170 | return implode(' ', $keyword); 171 | } 172 | 173 | return (string) $keyword; 174 | } 175 | 176 | /** 177 | * Get global search keyword. 178 | */ 179 | public function keyword(): string 180 | { 181 | /** @var string $keyword */ 182 | $keyword = $this->request->input('search.value') ?? ''; 183 | 184 | return $this->prepareKeyword($keyword); 185 | } 186 | 187 | /** 188 | * Get column name by index. 189 | */ 190 | public function columnName(int $i): ?string 191 | { 192 | /** @var string[] $column */ 193 | $column = $this->request->input("columns.$i"); 194 | 195 | return (isset($column['name']) && $column['name'] != '') ? $column['name'] : $column['data']; 196 | } 197 | 198 | /** 199 | * Check if DataTables allow pagination. 200 | */ 201 | public function isPaginationable(): bool 202 | { 203 | return ! is_null($this->request->input('start')) && 204 | ! is_null($this->request->input('length')) && 205 | $this->request->input('length') != -1; 206 | } 207 | 208 | public function getBaseRequest(): BaseRequest 209 | { 210 | return $this->request; 211 | } 212 | 213 | /** 214 | * Get starting record value. 215 | */ 216 | public function start(): int 217 | { 218 | $start = $this->request->input('start', 0); 219 | 220 | return is_numeric($start) ? intval($start) : 0; 221 | } 222 | 223 | /** 224 | * Get per page length. 225 | */ 226 | public function length(): int 227 | { 228 | $length = $this->request->input('length', 10); 229 | 230 | return is_numeric($length) ? intval($length) : 10; 231 | } 232 | 233 | /** 234 | * Get draw request. 235 | */ 236 | public function draw(): int 237 | { 238 | $draw = $this->request->input('draw', 0); 239 | 240 | return is_numeric($draw) ? intval($draw) : 0; 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/config/datatables.php: -------------------------------------------------------------------------------- 1 | [ 8 | /* 9 | * Smart search will enclose search keyword with wildcard string "%keyword%". 10 | * SQL: column LIKE "%keyword%" 11 | */ 12 | 'smart' => true, 13 | 14 | /* 15 | * Multi-term search will explode search keyword using spaces resulting into multiple term search. 16 | */ 17 | 'multi_term' => true, 18 | 19 | /* 20 | * Case insensitive will search the keyword in lower case format. 21 | * SQL: LOWER(column) LIKE LOWER(keyword) 22 | */ 23 | 'case_insensitive' => true, 24 | 25 | /* 26 | * Wild card will add "%" in between every characters of the keyword. 27 | * SQL: column LIKE "%k%e%y%w%o%r%d%" 28 | */ 29 | 'use_wildcards' => false, 30 | 31 | /* 32 | * Perform a search which starts with the given keyword. 33 | * SQL: column LIKE "keyword%" 34 | */ 35 | 'starts_with' => false, 36 | ], 37 | 38 | /* 39 | * DataTables internal index id response column name. 40 | */ 41 | 'index_column' => 'DT_RowIndex', 42 | 43 | /* 44 | * List of available builders for DataTables. 45 | * This is where you can register your custom DataTables builder. 46 | */ 47 | 'engines' => [ 48 | 'eloquent' => Yajra\DataTables\EloquentDataTable::class, 49 | 'query' => Yajra\DataTables\QueryDataTable::class, 50 | 'collection' => Yajra\DataTables\CollectionDataTable::class, 51 | 'resource' => Yajra\DataTables\ApiResourceDataTable::class, 52 | ], 53 | 54 | /* 55 | * DataTables accepted builder to engine mapping. 56 | * This is where you can override which engine a builder should use 57 | * Note, only change this if you know what you are doing! 58 | */ 59 | 'builders' => [ 60 | // Illuminate\Database\Eloquent\Relations\Relation::class => 'eloquent', 61 | // Illuminate\Database\Eloquent\Builder::class => 'eloquent', 62 | // Illuminate\Database\Query\Builder::class => 'query', 63 | // Illuminate\Support\Collection::class => 'collection', 64 | ], 65 | 66 | /* 67 | * Nulls last sql pattern for PostgreSQL & Oracle. 68 | * For MySQL, use 'CASE WHEN :column IS NULL THEN 1 ELSE 0 END, :column :direction' 69 | */ 70 | 'nulls_last_sql' => ':column :direction NULLS LAST', 71 | 72 | /* 73 | * User friendly message to be displayed on user if error occurs. 74 | * Possible values: 75 | * null - The exception message will be used on error response. 76 | * 'throw' - Throws a \Yajra\DataTables\Exceptions\Exception. Use your custom error handler if needed. 77 | * 'custom message' - Any friendly message to be displayed to the user. You can also use translation key. 78 | */ 79 | 'error' => env('DATATABLES_ERROR', null), 80 | 81 | /* 82 | * Default columns definition of DataTable utility functions. 83 | */ 84 | 'columns' => [ 85 | /* 86 | * List of columns hidden/removed on json response. 87 | */ 88 | 'excess' => ['rn', 'row_num'], 89 | 90 | /* 91 | * List of columns to be escaped. If set to *, all columns are escape. 92 | * Note: You can set the value to empty array to disable XSS protection. 93 | */ 94 | 'escape' => '*', 95 | 96 | /* 97 | * List of columns that are allowed to display html content. 98 | * Note: Adding columns to list will make us available to XSS attacks. 99 | */ 100 | 'raw' => ['action'], 101 | 102 | /* 103 | * List of columns are forbidden from being searched/sorted. 104 | */ 105 | 'blacklist' => ['password', 'remember_token'], 106 | 107 | /* 108 | * List of columns that are only allowed for search/sort. 109 | * If set to *, all columns are allowed. 110 | */ 111 | 'whitelist' => '*', 112 | ], 113 | 114 | /* 115 | * JsonResponse header and options config. 116 | */ 117 | 'json' => [ 118 | 'header' => [], 119 | 'options' => 0, 120 | ], 121 | 122 | /* 123 | * Default condition to determine if a parameter is a callback or not. 124 | * Callbacks needs to start by those terms, or they will be cast to string. 125 | */ 126 | 'callback' => ['$', '$.', 'function'], 127 | ]; 128 | -------------------------------------------------------------------------------- /src/helper.php: -------------------------------------------------------------------------------- 1 | make($source); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lumen.php: -------------------------------------------------------------------------------- 1 | basePath().'/config'.($path ? '/'.$path : $path); 13 | } 14 | } 15 | 16 | if (! function_exists('public_path')) { 17 | /** 18 | * Return the path to public dir. 19 | * 20 | * @param null $path 21 | * @return string 22 | */ 23 | function public_path($path = null) 24 | { 25 | return rtrim((string) app()->basePath('public/'.$path), '/'); 26 | } 27 | } 28 | --------------------------------------------------------------------------------