diff --git a/.gitattributes b/.gitattributes index 47ea9b35..be4c5df3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,7 @@ .gitattributes export-ignore .gitignore export-ignore .scrutinizer.yml export-ignore -.styleci.yml export-ignore phpstan.neon.dist export-ignore phpunit.xml.dist export-ignore +pint.json export-ignore +rector.php export-ignore diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 9ad5f7f9..971c16c8 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -1,23 +1,26 @@ -name: "Continuous Integration" +name: Continuous Integration on: push: + branches: + - master + - '*.x' pull_request: schedule: - cron: '0 0 * * *' jobs: - phpunit: + tests: runs-on: ubuntu-latest strategy: fail-fast: true matrix: - php: [8.1, 8.2, 8.3] - stability: [prefer-stable] + php: [ 8.2, 8.3, 8.4, 8.5 ] + stability: [ prefer-stable ] - name: PHP ${{ matrix.php }} - ${{ matrix.stability }} + name: PHP ${{ matrix.php }} - STABILITY ${{ matrix.stability }} steps: - name: Checkout code @@ -27,7 +30,6 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, memcached tools: composer:v2 coverage: none @@ -39,7 +41,7 @@ jobs: with: timeout_minutes: 5 max_attempts: 5 - command: COMPOSER_ROOT_VERSION=dev-master composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress + command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress - name: Execute tests run: vendor/bin/phpunit diff --git a/.github/workflows/pint.yml b/.github/workflows/pint.yml new file mode 100644 index 00000000..147b6e3a --- /dev/null +++ b/.github/workflows/pint.yml @@ -0,0 +1,29 @@ +name: PHP Linting +on: + pull_request: + push: + branches: + - master + - 11.x +jobs: + phplint: + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: "laravel-pint" + uses: aglipanci/laravel-pint-action@latest + with: + preset: laravel + verboseMode: true + + - uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "fix: pint :robot:" diff --git a/.github/workflows/semantic-release.yml b/.github/workflows/semantic-release.yml new file mode 100644 index 00000000..3581d7cf --- /dev/null +++ b/.github/workflows/semantic-release.yml @@ -0,0 +1,41 @@ +name: Semantic Releases + +on: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + issues: write + packages: write + statuses: write + +jobs: + run: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Configuration + run: | + if [ -n "$GH_TOKEN_SECRET" ]; then + echo "GH_TOKEN=$GH_TOKEN_SECRET" >> $GITHUB_ENV + else + echo "GH_TOKEN=$GITHUB_TOKEN" >> $GITHUB_ENV + fi + env: + GH_TOKEN_SECRET: ${{ secrets.GH_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + + - name: Semantic Release + uses: cycjimmy/semantic-release-action@9cc899c47e6841430bbaedb43de1560a568dfd16 + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 1f150293..b6933cea 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -1,21 +1,12 @@ -name: "Static Analysis" +name: Static Analysis on: push: - paths: - - .github/workflows/static-analysis.yml - - composer.* - - phpstan.neon.dist - - src/** - - tests/** + branches: + - master + - '*.x' pull_request: - paths: - - .github/workflows/static-analysis.yml - - composer.* - - phpstan.neon.dist - - src/** - - tests/** schedule: - cron: '0 0 * * *' @@ -23,32 +14,26 @@ on: jobs: static-analysis-phpstan: - name: "Static Analysis with PHPStan" + name: Source Code runs-on: ubuntu-latest - strategy: - fail-fast: true - matrix: - php: [8.1] - stability: [prefer-stable] - steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php }} + php-version: 8.2 tools: composer:v2 coverage: none - name: Install dependencies - uses: nick-invision/retry@v1 + uses: nick-fields/retry@v3 with: timeout_minutes: 5 max_attempts: 5 - command: COMPOSER_ROOT_VERSION=dev-master composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress + command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress - - name: "Run a static analysis with phpstan/phpstan" - run: "vendor/bin/phpstan --error-format=table" + - name: Run Static Analysis + run: vendor/bin/phpunit diff --git a/.releaserc.yml b/.releaserc.yml new file mode 100644 index 00000000..e96c554c --- /dev/null +++ b/.releaserc.yml @@ -0,0 +1,26 @@ +{ + "branches": [ + "main", + "master", + "*.x" + ], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + [ + "@semantic-release/changelog", + { + "changelogFile": "CHANGELOG.md" + } + ], + [ + "@semantic-release/git", + { + "assets": [ + "CHANGELOG.md" + ] + } + ], + "@semantic-release/github" + ] +} diff --git a/.styleci.yml b/.styleci.yml deleted file mode 100644 index 0285f179..00000000 --- a/.styleci.yml +++ /dev/null @@ -1 +0,0 @@ -preset: laravel diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a5bf9bd..91ad2c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,229 +1,90 @@ -# Laravel DataTables CHANGELOG +## [12.6.1](https://github.com/yajra/laravel-datatables/compare/v12.6.0...v12.6.1) (2025-10-11) -[![Latest Stable Version](https://poser.pugx.org/yajra/laravel-datatables-oracle/v/stable.png)](https://packagist.org/packages/yajra/laravel-datatables-oracle) -[![Total Downloads](https://poser.pugx.org/yajra/laravel-datatables-oracle/downloads.png)](https://packagist.org/packages/yajra/laravel-datatables-oracle) -[![Build Status](https://travis-ci.org/yajra/laravel-datatables.png?branch=master)](https://travis-ci.org/yajra/laravel-datatables) -[![Latest Unstable Version](https://poser.pugx.org/yajra/laravel-datatables-oracle/v/unstable.svg)](https://packagist.org/packages/yajra/laravel-datatables-oracle) -[![License](https://poser.pugx.org/yajra/laravel-datatables-oracle/license.svg)](https://packagist.org/packages/yajra/laravel-datatables-oracle) -### [Unreleased] - -### [v10.11.4] - 2024-02-28 - -- fix: EloquentDataTable return type typo #3123 - -### [v10.11.3] - 2023-12-27 - -- fix: Update composer.json to use Larastan Org #3107 - -### [v10.11.2] - 2023-12-12 - -- fix: scout search with smart search #3105 - -### [v10.11.1] - 2023-11-25 - -- fix: Prevent error when PHP extension iconv not enabled. #3098 - -### [v10.11.0] - 2023-11-04 - -- feat: Scout Search Implementation #3082 -- feat: Add scout fixed ordering for pgsql and oracle #3090 - -### [v10.10.0] - 2023-10-04 - -- feat: allow closure on formatColumn #3073 - -### [v10.9.0] - 2023-09-29 - -- feat: Ability to pass static data to a blade render #3067 - -### [v10.8.0] - 2023-08-12 - -- feat: convert prepareQuery from protected to public #3045 - -### [v10.7.0] - 2023-07-31 - -- feat: add ability to disable eloquent getter mutator #3009 -- feat: Ability to use deep relations for searching #3035 - -### [v10.6.2] - 2023-07-15 - -- fix: #3010 - convert expressions to strings #3029 +### Bug Fixes -### [v10.6.1] - 2023-07-05 +* value when mask uses "/" ([c771900](https://github.com/yajra/laravel-datatables/commit/c77190030c713e5b64c433bd161d9f33a210f22b)) -- fix: #3025 #3026 -- fix the error introduced in 10.4.4 as described in #3025. +# [12.6.0](https://github.com/yajra/laravel-datatables/compare/v12.5.1...v12.6.0) (2025-10-08) -### [v10.6.0] - 2023-06-29 -- feat: Expose autoFilter setter to disable post filtering #2981 - -### [v10.5.0] - 2023-06-29 +### Bug Fixes -- feat: Prevent editColumn when column is not shown #3018 +* replace unsafe eval() with Blade::render() in compileBlade ([7f46d58](https://github.com/yajra/laravel-datatables/commit/7f46d5872b0324493c28ecc8d848c182e88f30e0)) -### [v10.4.4] - 2023-06-27 -- feat: Optimize countQuery with complex select #3008 -- fix: phpstan #3022 +### Features -### [v10.4.3] - 2023-06-07 +* add __isset() method to Request for attribute existence check ([33f44d4](https://github.com/yajra/laravel-datatables/commit/33f44d42d284d6ea0a054de81ad5a57c3050867d)) -- Fix: Prevent the filteredCount() query if no filter is applied to the initial query #3007 +# Laravel DataTables -### [v10.4.2] - 2023-05-31 +## CHANGELOG -- Fix return type for setTransformer() and setSerializer() #3003 - -### [v10.4.1] - 2023-05-27 - -- fix: Error when setting language config for "editor" #2983 - -### [v10.4.0] - 2023-03-28 - -- feat: Allow any callable in ->addColumn() #2977 -- fix: #2976 - -### [v10.3.1] - 2023-02-20 - -- fix: Fix anonymous resource collection data formatting #2944 -- fix: phpunit 10 deprecation #2955 -- fix: bump orchestra/testbench to 8 #2949 - -### [v10.3.0] - 2023-02-07 - -- Add Laravel 10 compatibility #2948 - -### [v10.2.3] - 2023-01-18 - -- fix: Custom Order on eager loaded relationships was not working -- fix #2905 - -### [v10.2.2] - 2023-01-11 - -- fix: prevent deprecation errors in php 8.1+ #2931 -- fixes #2930 - -### [v10.2.1] - 2022-12-07 - -- fix: case insensitive starts with search #2917 #2916 - -### [v10.2.0] - 2022-11-03 - -- PHP 8.1 Depreciation Fix #2877 -- Methods pointing to the "uncustomizable" classes. #2861 - -### [v10.1.6] - 2022-10-10 - -- Fix anonymous resource collection #2870 -- Fix #2827 -- Add stale workflow - -### [v10.1.5] - 2022-10-06 - -- Fix with method error with static analysis #2865 +### [Unreleased] -### [v10.1.4] - 2022-09-27 +### v12.5.1 - 2025-10-02 -- Fixed the search column for same table relations #2856 +- fix: ambiguous column in columnControlSearch() method #3252 -### [v10.1.3] - 2022-09-20 +### v12.5.0 - 2025-10-01 -- Fix relation key name for BelongsToMany #2850 +- feat: server-side column control #3251 +- fix: https://github.com/yajra/laravel-datatables/issues/3250 -### [v10.1.2] - 2022-07-12 +### v12.4.2 - 2025-09-09 -- Fix HasOneThrough #2818 +- fix: remove @internal annotation from orderColumn() method #3248 -### [v10.1.1] - 2022-06-24 +### v12.4.1 - 2025-08-29 -- Fix null recordsFiltered on empty collection #2806 -- Fix #2793 +- fix: request handling with playwright / pest 4 #3247 -### [v10.1.0] - 2022-06-21 +### v12.4.0 - 2025-06-15 -- Add support for dependency injection when using closure. #2800 +- feat: add min search length control #3242 +- fix: #3241 -### [v10.0.8] - 2022-06-21 +### v12.3.1 - 2025-06-10 -- Make canCreate at QueryDataTable accept QueryBuilder only #2798 +- fix: support for array notation #3243 -### [v10.0.7] - 2022-05-23 +### v12.3.0 - 2025-05-17 -- Fix create eloquent datatable from relation #2789 +- feat: add option to enable alias on relation tables #3234 +- tests: Add tests to cover prefix detection #3239 +- fix: https://github.com/yajra/laravel-datatables/pull/1782 -### [v10.0.6] - 2022-05-18 +### v12.2.1 - 2025-05-09 -- Added null parameter type as allowed to handle default Action column from laravel-datatables-html #2787 +- fix: improve prefix detection #3238 +- fix: #3237 -### [v10.0.5] - 2022-05-17 +### v12.2.0 - 2025-05-08 -- Fix Return value must be of type int, string returned. +- feat: add relation resolver param to order callback #3232 +- fix: improve column alias detection #3236 +- fix: #3235 -### [v10.0.4] - 2022-05-08 +### v12.1.2 - 2025-05-07 -- Fix accidental formatter issue on eloquent -- Add formatColumn test for eloquent +- fix: prevent prefixing null/empty string #3233 -### [v10.0.3] - 2022-05-08 +### v12.1.1 - 2025-05-05 -- Additional fix & test for zero total records +- fix: prevent ambiguous column names #3227 -### [v10.0.2] - 2022-05-08 +### v12.1.0 - 2025-04-28 -- Fix set total & filtered records count https://github.com/yajra/laravel-datatables/pull/2778 -- Fix set total & filtered records count -- Fix #1453 #1454 #2050 #2609 -- Add feature test -- Deprecate `skipTotalRecords`, just use `setTotalRecords` directly. +- feat: add relation resolver param to filter callbacks #3229 -### [v10.0.1] - 2022-05-08 +### v12.0.1 - 2025-04-07 -- Code clean-up and several phpstan fixes +- fix: query results improvements #3224 -### [v10.0.0] - 2022-05-08 +### v12.0.0 - 2025-02-26 -- Laravel DataTables v10.x to support Laravel 9.x -- Added PHPStan with max level static analysis -- Drop `queryBuilder()` method -- Drop support for `ApiResourceDataTable` -- PHP8 syntax / method signature changed +- feat: Laravel v12 Compatibility #3217 +- fix: prevent duplicate table name errors #3216 -[Unreleased]: https://github.com/yajra/laravel-datatables/compare/v10.11.3...10.x -[v10.11.3]: https://github.com/yajra/laravel-datatables/compare/v10.11.3...v10.11.2 -[v10.11.2]: https://github.com/yajra/laravel-datatables/compare/v10.11.2...v10.11.1 -[v10.11.1]: https://github.com/yajra/laravel-datatables/compare/v10.11.1...v10.11.0 -[v10.11.0]: https://github.com/yajra/laravel-datatables/compare/v10.11.0...v10.10.0 -[v10.10.0]: https://github.com/yajra/laravel-datatables/compare/v10.10.0...v10.9.0 -[v10.9.0]: https://github.com/yajra/laravel-datatables/compare/v10.9.0...v10.8.0 -[v10.8.0]: https://github.com/yajra/laravel-datatables/compare/v10.8.0...v10.7.0 -[v10.7.0]: https://github.com/yajra/laravel-datatables/compare/v10.7.0...v10.6.2 -[v10.6.2]: https://github.com/yajra/laravel-datatables/compare/v10.6.2...v10.6.1 -[v10.6.1]: https://github.com/yajra/laravel-datatables/compare/v10.6.1...v10.6.0 -[v10.6.0]: https://github.com/yajra/laravel-datatables/compare/v10.6.0...v10.5.0 -[v10.5.0]: https://github.com/yajra/laravel-datatables/compare/v10.5.0...v10.4.4 -[v10.4.4]: https://github.com/yajra/laravel-datatables/compare/v10.4.4...v10.4.3 -[v10.3.1]: https://github.com/yajra/laravel-datatables/compare/v10.3.1...v10.3.0 -[v10.3.1]: https://github.com/yajra/laravel-datatables/compare/v10.3.1...v10.3.0 -[v10.3.0]: https://github.com/yajra/laravel-datatables/compare/v10.3.0...v10.2.3 -[v10.2.3]: https://github.com/yajra/laravel-datatables/compare/v10.2.3...v10.2.2 -[v10.2.2]: https://github.com/yajra/laravel-datatables/compare/v10.2.2...v10.2.1 -[v10.2.1]: https://github.com/yajra/laravel-datatables/compare/v10.2.1...v10.2.0 -[v10.2.0]: https://github.com/yajra/laravel-datatables/compare/v10.2.0...v10.1.6 -[v10.1.6]: https://github.com/yajra/laravel-datatables/compare/v10.1.6...v10.1.5 -[v10.1.5]: https://github.com/yajra/laravel-datatables/compare/v10.1.5...v10.1.4 -[v10.1.4]: https://github.com/yajra/laravel-datatables/compare/v10.1.4...v10.1.3 -[v10.1.3]: https://github.com/yajra/laravel-datatables/compare/v10.1.3...v10.1.2 -[v10.1.2]: https://github.com/yajra/laravel-datatables/compare/v10.1.2...v10.1.1 -[v10.1.1]: https://github.com/yajra/laravel-datatables/compare/v10.1.1...v10.1.0 -[v10.1.0]: https://github.com/yajra/laravel-datatables/compare/v10.1.0...v10.0.8 -[v10.0.8]: https://github.com/yajra/laravel-datatables/compare/v10.0.8...v10.0.7 -[v10.0.7]: https://github.com/yajra/laravel-datatables/compare/v10.0.7...v10.0.6 -[v10.0.6]: https://github.com/yajra/laravel-datatables/compare/v10.0.6...v10.0.5 -[v10.0.5]: https://github.com/yajra/laravel-datatables/compare/v10.0.5...v10.0.4 -[v10.0.4]: https://github.com/yajra/laravel-datatables/compare/v10.0.4...v10.0.3 -[v10.0.3]: https://github.com/yajra/laravel-datatables/compare/v10.0.3...v10.0.2 -[v10.0.2]: https://github.com/yajra/laravel-datatables/compare/v10.0.2...v10.0.1 -[v10.0.1]: https://github.com/yajra/laravel-datatables/compare/v10.0.1...v10.0.0 -[v10.0.0]: https://github.com/yajra/laravel-datatables/compare/v10.0.0...10.x +[Unreleased]: https://github.com/yajra/laravel-datatables/compare/v12.0.0...master diff --git a/README.md b/README.md index f31f2175..32cff155 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,74 @@ -# jQuery DataTables API for Laravel 4|5|6|7|8|9|10 +# jQuery DataTables API for Laravel [![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) [![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://www.paypal.me/yajra) [![Donate](https://img.shields.io/badge/donate-patreon-blue.svg)](https://www.patreon.com/bePatron?u=4521203) -[![Laravel 4.2|5.x|6|7|8|9|10](https://img.shields.io/badge/Laravel-4.2|5.x|6|7|8|9|10-orange.svg)](http://laravel.com) +[![Laravel 12](https://img.shields.io/badge/Laravel-12-orange.svg)](http://laravel.com) [![Latest Stable Version](https://img.shields.io/packagist/v/yajra/laravel-datatables-oracle.svg)](https://packagist.org/packages/yajra/laravel-datatables-oracle) [![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) [![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) -[![Total Downloads](https://poser.pugx.org/yajra/laravel-datatables-oracle/downloads.png)](https://packagist.org/packages/yajra/laravel-datatables-oracle) + +[![Total Downloads](https://poser.pugx.org/yajra/laravel-datatables-oracle/d/total.svg)](https://packagist.org/packages/yajra/laravel-datatables-oracle) [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://packagist.org/packages/yajra/laravel-datatables-oracle) 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. ```php -return datatables()->eloquent(User::query())->toJson(); -return datatables()->query(DB::table('users'))->toJson(); -return datatables()->collection(User::all())->toJson(); +use Yajra\DataTables\Facades\DataTables; + +return DataTables::eloquent(User::query())->toJson(); +return DataTables::query(DB::table('users'))->toJson(); +return DataTables::collection(User::all())->toJson(); -return datatables(User::query())->toJson(); -return datatables(DB::table('users'))->toJson(); -return datatables(User::all())->toJson(); +return DataTables::make(User::query())->toJson(); +return DataTables::make(DB::table('users'))->toJson(); +return DataTables::make(User::all())->toJson(); ``` ## Sponsors - - DataTables - - - - JetBrains.com - - - - Blackfire.io - - - + + + + + + + +
+ DataTables Logo + A big thank you to DataTables for supporting this project with a free DataTables Editor license.
+ + + + + + + + +
+ JetBrains logo. + A big thank you to JetBrains for supporting this project with free open-source licenses of their IDEs.
+ + + + + + + + +
Blackfire.io LogoA big thank you to Blackfire.io for supporting this project with a free open-source license.
## Requirements -- [PHP >= 8.0.2](http://php.net/) +- [PHP >= 8.2](http://php.net/) - [Laravel Framework](https://github.com/laravel/framework) -- [jQuery DataTables v1.10.x](http://datatables.net/) +- [DataTables](http://datatables.net/) ## Documentations - [Github Docs](https://github.com/yajra/laravel-datatables-docs) - [Laravel DataTables Quick Starter](https://yajrabox.com/docs/laravel-datatables/master/quick-starter) - [Laravel DataTables Documentation](https://yajrabox.com/docs/laravel-datatables) -- [Laravel 5.0 - 5.3 Demo Application](https://datatables.yajrabox.com) ## Laravel Version Compatibility @@ -65,21 +84,31 @@ return datatables(User::all())->toJson(); | 5.6.x | 8.x | | 5.7.x | 8.x | | 5.8.x | 9.x | -| 6.x.x | 9.x | -| 7.x.x | 9.x | -| 8.x.x | 9.x | -| 9.x.x | 10.x | -| 10.x.x | 10.x | +| 6.x | 9.x | +| 7.x | 9.x | +| 8.x | 9.x | +| 9.x | 10.x | +| 10.x | 10.x | +| 11.x | 11.x | +| 12.x | 12.x | ## Quick Installation +### Option 1: Install all DataTables libraries + +```bash +composer require yajra/laravel-datatables:"^12" +``` + +### Option 2: Install only this library + ```bash -composer require yajra/laravel-datatables-oracle:"^10.0" +composer require yajra/laravel-datatables-oracle:"^12" ``` #### Service Provider & Facade (Optional on Laravel 5.5+) -Register provider and facade on your `config/app.php` file. +Register the provider and facade on your `config/app.php` file. ```php 'providers' => [ ..., @@ -104,11 +133,12 @@ And that's it! Start building out some awesome DataTables! To enable debugging mode, just set `APP_DEBUG=true` and the package will include the queries and inputs used when processing the table. -**IMPORTANT:** Please make sure that APP_DEBUG is set to false when your app is on production. +> [!IMPORTANT] +> Please ensure that the `APP_DEBUG` config is set to false when your app is in production. ## PHP ARTISAN SERVE BUG -Please avoid using `php artisan serve` when developing with the package. +Please avoid using `php artisan serve` when developing the package. 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. 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. @@ -119,7 +149,7 @@ Please see [CONTRIBUTING](https://github.com/yajra/laravel-datatables/blob/maste ## Security -If you discover any security related issues, please email [aqangeles@gmail.com](mailto:aqangeles@gmail.com) instead of using the issue tracker. +If you discover any security-related issues, please email [aqangeles@gmail.com](mailto:aqangeles@gmail.com) instead of using the issue tracker. ## Credits diff --git a/UPGRADE.md b/UPGRADE.md index 9ddec173..36a386b5 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,9 @@ # UPGRADE GUIDE +## Upgrading from v11.x to v12.x + +- See PR https://github.com/yajra/laravel-datatables/pull/3216 + ## Upgrading from v9.x to v10.x - `ApiResourceDataTable` support dropped, use `CollectionDataTable` instead. diff --git a/composer.json b/composer.json index 04b884df..6e034353 100644 --- a/composer.json +++ b/composer.json @@ -1,82 +1,93 @@ { - "name": "yajra/laravel-datatables-oracle", - "description": "jQuery DataTables API for Laravel 4|5|6|7|8|9|10", - "keywords": [ - "laravel", - "dataTables", - "jquery" - ], - "license": "MIT", - "authors": [ - { - "name": "Arjay Angeles", - "email": "aqangeles@gmail.com" - } - ], - "require": { - "php": "^8.0.2", - "illuminate/database": "^9|^10", - "illuminate/filesystem": "^9|^10", - "illuminate/http": "^9|^10", - "illuminate/support": "^9|^10", - "illuminate/view": "^9|^10" - }, - "require-dev": { - "algolia/algoliasearch-client-php": "^3.4", - "laravel/scout": "^10.5", - "meilisearch/meilisearch-php": "^1.4", - "larastan/larastan": "^2.4", - "orchestra/testbench": "^8", - "yajra/laravel-datatables-html": "^9.3.4|^10" - }, - "suggest": { - "yajra/laravel-datatables-export": "Plugin for server-side exporting using livewire and queue worker.", - "yajra/laravel-datatables-buttons": "Plugin for server-side exporting of dataTables.", - "yajra/laravel-datatables-html": "Plugin for server-side HTML builder of dataTables.", - "yajra/laravel-datatables-fractal": "Plugin for server-side response using Fractal.", - "yajra/laravel-datatables-editor": "Plugin to use DataTables Editor (requires a license)." - }, - "autoload": { - "psr-4": { - "Yajra\\DataTables\\": "src/" + "name": "yajra/laravel-datatables-oracle", + "description": "jQuery DataTables API for Laravel", + "keywords": [ + "yajra", + "laravel", + "dataTables", + "jquery" + ], + "license": "MIT", + "authors": [ + { + "name": "Arjay Angeles", + "email": "aqangeles@gmail.com" + } + ], + "require": { + "php": "^8.2", + "illuminate/database": "^12", + "illuminate/filesystem": "^12", + "illuminate/http": "^12", + "illuminate/support": "^12", + "illuminate/view": "^12" }, - "files": [ - "src/helper.php" - ] - }, - "autoload-dev": { - "psr-4": { - "Yajra\\DataTables\\Tests\\": "tests/" - } - }, - "extra": { - "branch-alias": { - "dev-master": "10.x-dev" + "require-dev": { + "algolia/algoliasearch-client-php": "^3.4.1", + "larastan/larastan": "^3.1.0", + "laravel/pint": "^1.14", + "laravel/scout": "^10.8.3", + "meilisearch/meilisearch-php": "^1.6.1", + "orchestra/testbench": "^10", + "rector/rector": "^2.0" + }, + "suggest": { + "yajra/laravel-datatables-export": "Plugin for server-side exporting using livewire and queue worker.", + "yajra/laravel-datatables-buttons": "Plugin for server-side exporting of dataTables.", + "yajra/laravel-datatables-html": "Plugin for server-side HTML builder of dataTables.", + "yajra/laravel-datatables-fractal": "Plugin for server-side response using Fractal.", + "yajra/laravel-datatables-editor": "Plugin to use DataTables Editor (requires a license)." + }, + "autoload": { + "psr-4": { + "Yajra\\DataTables\\": "src/" + }, + "files": [ + "src/helper.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Yajra\\DataTables\\Tests\\": "tests/" + } }, - "laravel": { - "providers": [ - "Yajra\\DataTables\\DataTablesServiceProvider" - ], - "aliases": { - "DataTables": "Yajra\\DataTables\\Facades\\DataTables" - } - } - }, - "config": { - "sort-packages": true, - "allow-plugins": { - "php-http/discovery": true - } - }, - "scripts": { - "test": "vendor/bin/phpunit" - }, - "minimum-stability": "dev", - "prefer-stable": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/yajra" - } - ] + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + }, + "laravel": { + "providers": [ + "Yajra\\DataTables\\DataTablesServiceProvider" + ], + "aliases": { + "DataTables": "Yajra\\DataTables\\Facades\\DataTables" + } + } + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "php-http/discovery": true + } + }, + "scripts": { + "test": "./vendor/bin/phpunit", + "pint": "./vendor/bin/pint", + "rector": "./vendor/bin/rector", + "stan": "./vendor/bin/phpstan analyse --memory-limit=2G --ansi --no-progress --no-interaction --configuration=phpstan.neon.dist", + "pr": [ + "@rector", + "@pint", + "@stan", + "@test" + ] + }, + "minimum-stability": "dev", + "prefer-stable": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/yajra" + } + ] } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 9971e78a..26779953 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -10,8 +10,21 @@ parameters: ignoreErrors: - '#Unsafe usage of new static\(\).#' + - identifier: missingType.iterableValue + - identifier: argument.type + - identifier: cast.string + - identifier: foreach.nonIterable + - identifier: binaryOp.invalid + - identifier: offsetAccess.nonOffsetAccessible + - identifier: return.type + - identifier: method.nonObject + - identifier: varTag.nativeType + - identifier: assign.propertyType + - identifier: callable.nonCallable + - identifier: property.nonObject excludePaths: - src/helper.php - checkMissingIterableValueType: false + noEnvCallsOutsideOfConfig: false + treatPhpDocTypesAsCertain: false diff --git a/pint.json b/pint.json new file mode 100644 index 00000000..93061b6b --- /dev/null +++ b/pint.json @@ -0,0 +1,3 @@ +{ + "preset": "laravel" +} diff --git a/rector.php b/rector.php new file mode 100644 index 00000000..ca7ca051 --- /dev/null +++ b/rector.php @@ -0,0 +1,22 @@ +paths([ + __DIR__.'/src', + __DIR__.'/tests', + ]); + + // register a single rule + $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); + + // define sets of rules + $rectorConfig->sets([ + LevelSetList::UP_TO_PHP_82, + ]); +}; diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index 8edc4cdc..00000000 --- a/sonar-project.properties +++ /dev/null @@ -1,3 +0,0 @@ -# Define separate root directories for sources and tests -sonar.sources = src/ -sonar.tests = tests/ diff --git a/src/CollectionDataTable.php b/src/CollectionDataTable.php index 99e7da1a..4392549e 100644 --- a/src/CollectionDataTable.php +++ b/src/CollectionDataTable.php @@ -13,13 +13,6 @@ class CollectionDataTable extends DataTableAbstract { - /** - * Collection object. - * - * @var \Illuminate\Support\Collection - */ - public Collection $collection; - /** * Collection object. * @@ -29,8 +22,6 @@ class CollectionDataTable extends DataTableAbstract /** * The offset of the first record in the full dataset. - * - * @var int */ private int $offset = 0; @@ -39,22 +30,18 @@ class CollectionDataTable extends DataTableAbstract * * @param \Illuminate\Support\Collection $collection */ - public function __construct(Collection $collection) + public function __construct(public Collection $collection) { $this->request = app('datatables.request'); $this->config = app('datatables.config'); - $this->collection = $collection; - $this->original = $collection; - $this->columns = array_keys($this->serialize($collection->first())); + $this->original = $this->collection; + $this->columns = array_keys($this->serialize($this->collection->first())); } /** * Serialize collection. - * - * @param mixed $collection - * @return array */ - protected function serialize($collection): array + protected function serialize(mixed $collection): array { return $collection instanceof Arrayable ? $collection->toArray() : (array) $collection; } @@ -87,8 +74,6 @@ public static function create($source) /** * Count results. - * - * @return int */ public function count(): int { @@ -97,8 +82,6 @@ public function count(): int /** * Perform column search. - * - * @return void */ public function columnSearch(): void { @@ -143,8 +126,6 @@ function ($row) use ($column, $keyword, $regex) { /** * Perform pagination. - * - * @return void */ public function paging(): void { @@ -157,14 +138,13 @@ public function paging(): void /** * Organizes works. * - * @param bool $mDataSupport - * @return \Illuminate\Http\JsonResponse - * * @throws \Exception */ - public function make($mDataSupport = true): JsonResponse + public function make(bool $mDataSupport = true): JsonResponse { try { + $this->validateMinLengthSearch(); + $this->totalRecords = $this->totalCount(); if ($this->totalRecords) { @@ -200,7 +180,6 @@ public function results(): Collection * Revert transformed DT_RowIndex back to its original values. * * @param bool $mDataSupport - * @return void */ private function revertIndexColumn($mDataSupport): void { @@ -222,7 +201,6 @@ private function revertIndexColumn($mDataSupport): void * the FULL dataset the collection was sliced from. It effectively allows the * collection to be "pre-sliced". * - * @param int $offset * @return static */ public function setOffset(int $offset): self @@ -234,9 +212,6 @@ public function setOffset(int $offset): self /** * Perform global search for the given keyword. - * - * @param string $keyword - * @return void */ protected function globalSearch(string $keyword): void { @@ -264,8 +239,6 @@ protected function globalSearch(string $keyword): void /** * Perform default query orderBy clause. - * - * @return void */ protected function defaultOrdering(): void { @@ -274,9 +247,7 @@ protected function defaultOrdering(): void $sorter = $this->getSorter($criteria); $this->collection = $this->collection - ->map(function ($data) { - return Arr::dot($data); - }) + ->map(fn ($data) => Arr::dot($data)) ->sort($sorter) ->map(function ($data) { foreach ($data as $key => $value) { @@ -291,9 +262,6 @@ protected function defaultOrdering(): void /** * Get array sorter closure. - * - * @param array $criteria - * @return \Closure */ protected function getSorter(array $criteria): Closure { diff --git a/src/Contracts/DataTable.php b/src/Contracts/DataTable.php index 29e167e4..d1320b4c 100644 --- a/src/Contracts/DataTable.php +++ b/src/Contracts/DataTable.php @@ -10,21 +10,17 @@ interface DataTable /** * Get results. * - * @return \Illuminate\Support\Collection + * @return \Illuminate\Support\Collection|\Illuminate\Support\Collection */ public function results(): Collection; /** * Count results. - * - * @return int */ public function count(): int; /** * Count total items. - * - * @return int */ public function totalCount(): int; @@ -32,45 +28,32 @@ public function totalCount(): int; * Set auto filter off and run your own filter. * Overrides global search. * - * @param callable $callback - * @param bool $globalSearch * @return static */ - public function filter(callable $callback, $globalSearch = false): self; + public function filter(callable $callback, bool $globalSearch = false): self; /** * Perform global search. - * - * @return void */ public function filtering(): void; /** * Perform column search. - * - * @return void */ public function columnSearch(): void; /** * Perform pagination. - * - * @return void */ public function paging(): void; /** * Perform sorting of columns. - * - * @return void */ public function ordering(): void; /** * Organizes works. - * - * @param bool $mDataSupport - * @return \Illuminate\Http\JsonResponse */ - public function make($mDataSupport = true): JsonResponse; + public function make(bool $mDataSupport = true): JsonResponse; } diff --git a/src/Contracts/Formatter.php b/src/Contracts/Formatter.php index 52c38e5b..9c9f50a0 100644 --- a/src/Contracts/Formatter.php +++ b/src/Contracts/Formatter.php @@ -5,9 +5,8 @@ interface Formatter { /** - * @param mixed $value * @param array|\Illuminate\Database\Eloquent\Model|object $row * @return string */ - public function format($value, $row); + public function format(mixed $value, $row); } diff --git a/src/DataTableAbstract.php b/src/DataTableAbstract.php index e3d6ca86..0e51ed2b 100644 --- a/src/DataTableAbstract.php +++ b/src/DataTableAbstract.php @@ -28,27 +28,18 @@ abstract class DataTableAbstract implements DataTable /** * DataTables Request object. - * - * @var \Yajra\DataTables\Utilities\Request */ public Utilities\Request $request; - /** - * @var \Psr\Log\LoggerInterface|null - */ protected ?LoggerInterface $logger = null; /** * Array of result columns/fields. - * - * @var array|null */ protected ?array $columns = []; /** * DT columns definitions container (add/edit/remove/filter/order/escape). - * - * @var array */ protected array $columnDef = [ 'index' => false, @@ -64,29 +55,26 @@ abstract class DataTableAbstract implements DataTable /** * Extra/Added columns. - * - * @var array */ protected array $extraColumns = []; /** * Total records. - * - * @var int|null */ protected ?int $totalRecords = null; /** * Total filtered records. - * - * @var int|null */ protected ?int $filteredRecords = null; + /** + * Flag to check if the total records count should be skipped. + */ + protected bool $skipTotalRecords = false; + /** * Auto-filter flag. - * - * @var bool */ protected bool $autoFilter = true; @@ -99,8 +87,6 @@ abstract class DataTableAbstract implements DataTable /** * DT row templates container. - * - * @var array */ protected array $templates = [ 'DT_RowId' => '', @@ -118,44 +104,32 @@ abstract class DataTableAbstract implements DataTable /** * Skip pagination as needed. - * - * @var bool */ protected bool $skipPaging = false; /** * Array of data to append on json response. - * - * @var array */ protected array $appends = []; - /** - * @var \Yajra\DataTables\Utilities\Config - */ protected Utilities\Config $config; - /** - * @var mixed - */ protected mixed $serializer; - /** - * @var array - */ protected array $searchPanes = []; protected mixed $transformer; protected bool $editOnlySelectedColumns = false; + protected int $minSearchLength = 0; + /** * Can the DataTable engine be created with these parameters. * - * @param mixed $source * @return bool */ - public static function canCreate($source) + public static function canCreate(mixed $source) { return false; } @@ -163,10 +137,9 @@ public static function canCreate($source) /** * Factory method, create and return an instance for the DataTable engine. * - * @param mixed $source * @return static */ - public static function create($source) + public static function create(mixed $source) { return new static($source); } @@ -194,9 +167,7 @@ public function formatColumn($columns, $formatter): static foreach ((array) $columns as $column) { $this->addColumn( $column.'_formatted', - function ($row) use ($column, $formatter) { - return $formatter(data_get($row, $column), $row); - } + fn ($row) => $formatter(data_get($row, $column), $row) ); } @@ -206,9 +177,7 @@ function ($row) use ($column, $formatter) { foreach ((array) $columns as $column) { $this->addColumn( $column.'_formatted', - function ($row) use ($column) { - return data_get($row, $column); - } + fn ($row) => data_get($row, $column) ); } @@ -292,8 +261,6 @@ public function removeColumn(): static /** * Get columns definition. - * - * @return array */ protected function getColumnsDefinition(): array { @@ -306,7 +273,6 @@ protected function getColumnsDefinition(): array /** * Get only selected columns in response. * - * @param array $columns * @return $this */ public function only(array $columns = []): static @@ -332,7 +298,6 @@ public function escapeColumns($columns = '*'): static /** * Add a makeHidden() to the row object. * - * @param array $attributes * @return $this */ public function makeHidden(array $attributes = []): static @@ -346,7 +311,6 @@ public function makeHidden(array $attributes = []): static /** * Add a makeVisible() to the row object. * - * @param array $attributes * @return $this */ public function makeVisible(array $attributes = []): static @@ -361,7 +325,6 @@ public function makeVisible(array $attributes = []): static * Set columns that should not be escaped. * Optionally merge the defaults from config. * - * @param array $columns * @param bool $merge * @return $this */ @@ -410,7 +373,6 @@ public function setRowId($content): static /** * Set DT_RowData templates. * - * @param array $data * @return $this */ public function setRowData(array $data): static @@ -438,7 +400,6 @@ public function addRowData($key, $value): static * Set DT_RowAttr templates. * result: . * - * @param array $data * @return $this */ public function setRowAttr(array $data): static @@ -465,11 +426,9 @@ public function addRowAttr($key, $value): static /** * Append data on json response. * - * @param mixed $key - * @param mixed $value * @return $this */ - public function with($key, $value = ''): static + public function with(mixed $key, mixed $value = ''): static { if (is_array($key)) { $this->appends = $key; @@ -483,8 +442,6 @@ public function with($key, $value = ''): static /** * Add with query callback value on response. * - * @param string $key - * @param callable $value * @return $this */ public function withQuery(string $key, callable $value): static @@ -497,7 +454,6 @@ public function withQuery(string $key, callable $value): static /** * Override default ordering method with a closure callback. * - * @param callable $closure * @return $this */ public function order(callable $closure): static @@ -510,7 +466,6 @@ public function order(callable $closure): static /** * Update list of columns that is not allowed for search/sort. * - * @param array $blacklist * @return $this */ public function blacklist(array $blacklist): static @@ -523,7 +478,6 @@ public function blacklist(array $blacklist): static /** * Update list of columns that is allowed for search/sort. * - * @param string|array $whitelist * @return $this */ public function whitelist(array|string $whitelist = '*'): static @@ -536,7 +490,6 @@ public function whitelist(array|string $whitelist = '*'): static /** * Set smart search config at runtime. * - * @param bool $state * @return $this */ public function smart(bool $state = true): static @@ -549,7 +502,6 @@ public function smart(bool $state = true): static /** * Set starts_with search config at runtime. * - * @param bool $state * @return $this */ public function startsWithSearch(bool $state = true): static @@ -562,7 +514,6 @@ public function startsWithSearch(bool $state = true): static /** * Set multi_term search config at runtime. * - * @param bool $multiTerm * @return $this */ public function setMultiTerm(bool $multiTerm = true): static @@ -575,7 +526,6 @@ public function setMultiTerm(bool $multiTerm = true): static /** * Set total records manually. * - * @param int $total * @return $this */ public function setTotalRecords(int $total): static @@ -590,12 +540,11 @@ public function setTotalRecords(int $total): static * This will improve the performance by skipping the total count query. * * @return $this - * - * @deprecated Just use setTotalRecords instead. */ public function skipTotalRecords(): static { $this->totalRecords = 0; + $this->skipTotalRecords = true; return $this; } @@ -603,7 +552,6 @@ public function skipTotalRecords(): static /** * Set filtered records manually. * - * @param int $total * @return $this */ public function setFilteredRecords(int $total): static @@ -656,7 +604,6 @@ public function pushToBlacklist($column): static * Check if column is blacklisted. * * @param string $column - * @return bool */ protected function isBlacklisted($column): bool { @@ -675,8 +622,6 @@ protected function isBlacklisted($column): bool /** * Perform sorting of columns. - * - * @return void */ public function ordering(): void { @@ -696,8 +641,6 @@ abstract protected function resolveCallbackParameter(); /** * Perform default query orderBy clause. - * - * @return void */ abstract protected function defaultOrdering(): void; @@ -705,11 +648,9 @@ abstract protected function defaultOrdering(): void; * Set auto filter off and run your own filter. * Overrides global search. * - * @param callable $callback - * @param bool $globalSearch * @return $this */ - public function filter(callable $callback, $globalSearch = false): static + public function filter(callable $callback, bool $globalSearch = false): self { $this->autoFilter = $globalSearch; $this->filterCallback = $callback; @@ -736,11 +677,9 @@ public function toJson($options = 0) * Add a search pane options on response. * * @param string $column - * @param mixed $options - * @param callable|null $builder * @return $this */ - public function searchPane($column, $options, callable $builder = null): static + public function searchPane($column, mixed $options, ?callable $builder = null): static { $options = value($options); @@ -756,18 +695,29 @@ public function searchPane($column, $options, callable $builder = null): static /** * Convert instance to array. - * - * @return array */ public function toArray(): array { return (array) $this->make()->getData(true); } + /** + * Count total items. + */ + public function totalCount(): int + { + return $this->totalRecords ??= $this->count(); + } + + public function editOnlySelectedColumns(): static + { + $this->editOnlySelectedColumns = true; + + return $this; + } + /** * Perform necessary filters. - * - * @return void */ protected function filterRecords(): void { @@ -780,14 +730,18 @@ protected function filterRecords(): void } $this->columnSearch(); + $this->columnControlSearch(); $this->searchPanesSearch(); $this->filteredCount(); } + public function columnControlSearch(): void + { + // Not implemented in the abstract class. + } + /** * Perform global search. - * - * @return void */ public function filtering(): void { @@ -807,14 +761,11 @@ public function filtering(): void * individual words and searches for each of them. * * @param string $keyword - * @return void */ protected function smartGlobalSearch($keyword): void { collect(explode(' ', $keyword)) - ->reject(function ($keyword) { - return trim($keyword) === ''; - }) + ->reject(fn ($keyword) => trim((string) $keyword) === '') ->each(function ($keyword) { $this->globalSearch($keyword); }); @@ -822,46 +773,27 @@ protected function smartGlobalSearch($keyword): void /** * Perform global search for the given keyword. - * - * @param string $keyword - * @return void */ abstract protected function globalSearch(string $keyword): void; /** * Perform search using search pane values. - * - * @return void */ protected function searchPanesSearch(): void { // Add support for search pane. } - /** - * Count total items. - * - * @return int - */ - public function totalCount(): int - { - return $this->totalRecords ??= $this->count(); - } - /** * Count filtered items. - * - * @return int */ - protected function filteredCount(): int + public function filteredCount(): int { return $this->filteredRecords ??= $this->count(); } /** * Apply pagination. - * - * @return void */ protected function paginate(): void { @@ -875,7 +807,6 @@ protected function paginate(): void * * @param iterable $results * @param array $processed - * @return array */ protected function transform($results, $processed): array { @@ -895,7 +826,6 @@ protected function transform($results, $processed): array * * @param iterable $results * @param bool $object - * @return array * * @throws \Exception */ @@ -913,9 +843,6 @@ protected function processResults($results, $object = false): array /** * Render json response. - * - * @param array $data - * @return \Illuminate\Http\JsonResponse */ protected function render(array $data): JsonResponse { @@ -944,9 +871,6 @@ protected function render(array $data): JsonResponse /** * Attach custom with meta on response. - * - * @param array $data - * @return array */ protected function attachAppends(array $data): array { @@ -955,9 +879,6 @@ protected function attachAppends(array $data): array /** * Append debug parameters on output. - * - * @param array $output - * @return array */ protected function showDebugger(array $output): array { @@ -969,12 +890,9 @@ protected function showDebugger(array $output): array /** * Return an error json response. * - * @param \Exception $exception - * @return \Illuminate\Http\JsonResponse - * * @throws \Yajra\DataTables\Exceptions\Exception|\Exception */ - protected function errorResponse(\Exception $exception) + protected function errorResponse(\Exception $exception): JsonResponse { /** @var string $error */ $error = $this->config->get('datatables.error'); @@ -991,7 +909,7 @@ protected function errorResponse(\Exception $exception) 'recordsTotal' => $this->totalRecords, 'recordsFiltered' => 0, 'data' => [], - 'error' => $error ? __($error) : "Exception Message:\n\n".$exception->getMessage(), + 'error' => $error ? __($error) : 'Exception Message:'.PHP_EOL.PHP_EOL.$exception->getMessage(), ]); } @@ -1010,7 +928,6 @@ public function getLogger() /** * Set monolog/logger instance. * - * @param \Psr\Log\LoggerInterface $logger * @return $this */ public function setLogger(LoggerInterface $logger): static @@ -1022,9 +939,6 @@ public function setLogger(LoggerInterface $logger): static /** * Setup search keyword. - * - * @param string $value - * @return string */ protected function setupKeyword(string $value): string { @@ -1042,11 +956,7 @@ protected function setupKeyword(string $value): string } /** - * Get column name to be use for filtering and sorting. - * - * @param int $index - * @param bool $wantsAlias - * @return string|null + * Get column name to be used for filtering and sorting. */ protected function getColumnName(int $index, bool $wantsAlias = false): ?string { @@ -1070,9 +980,6 @@ protected function getColumnName(int $index, bool $wantsAlias = false): ?string /** * Get column name by order column index. - * - * @param int $index - * @return string */ protected function getColumnNameByIndex(int $index): string { @@ -1085,18 +992,31 @@ protected function getColumnNameByIndex(int $index): string /** * If column name could not be resolved then use primary key. - * - * @return string */ protected function getPrimaryKeyName(): string { return 'id'; } - public function editOnlySelectedColumns(): static + public function minSearchLength(int $length): static { - $this->editOnlySelectedColumns = true; + $this->minSearchLength = $length; return $this; } + + protected function validateMinLengthSearch(): void + { + if ($this->request->isSearchable() + && $this->minSearchLength > 0 + && Str::length($this->request->keyword()) < $this->minSearchLength + ) { + $this->totalRecords = 0; + $this->filteredRecords = 0; + throw new \Exception( + __('Please enter at least :length characters to search.', ['length' => $this->minSearchLength]), + 400 + ); + } + } } diff --git a/src/DataTables.php b/src/DataTables.php index 2a45f909..724b44b3 100644 --- a/src/DataTables.php +++ b/src/DataTables.php @@ -6,7 +6,8 @@ use Illuminate\Contracts\Database\Query\Builder as QueryBuilder; use Illuminate\Support\Traits\Macroable; use Yajra\DataTables\Exceptions\Exception; -use Yajra\DataTables\Html\Builder; +use Yajra\DataTables\Utilities\Config; +use Yajra\DataTables\Utilities\Request; class DataTables { @@ -14,18 +15,9 @@ class DataTables /** * DataTables request object. - * - * @var \Yajra\DataTables\Utilities\Request */ protected Utilities\Request $request; - /** - * HTML builder instance. - * - * @var \Yajra\DataTables\Html\Builder|null - */ - protected ?Builder $html = null; - /** * Make a DataTable instance from source. * Alias of make for backward compatibility. @@ -81,38 +73,33 @@ public static function make($source) } } - throw new Exception('No available engine for '.get_class($source)); + throw new Exception('No available engine for '.$source::class); } /** * Get request object. - * - * @return \Yajra\DataTables\Utilities\Request */ - public function getRequest() + public function getRequest(): Request { return app('datatables.request'); } /** * Get config instance. - * - * @return \Yajra\DataTables\Utilities\Config */ - public function getConfig() + public function getConfig(): Config { return app('datatables.config'); } /** - * DataTables using Query. + * DataTables using query builder. * - * @param QueryBuilder $builder - * @return \Yajra\DataTables\QueryDataTable + * @throws \Yajra\DataTables\Exceptions\Exception */ public function query(QueryBuilder $builder): QueryDataTable { - /** @var string */ + /** @var string $dataTable */ $dataTable = config('datatables.engines.query'); $this->validateDataTable($dataTable, QueryDataTable::class); @@ -123,12 +110,11 @@ public function query(QueryBuilder $builder): QueryDataTable /** * DataTables using Eloquent Builder. * - * @param \Illuminate\Contracts\Database\Eloquent\Builder $builder - * @return \Yajra\DataTables\EloquentDataTable + * @throws \Yajra\DataTables\Exceptions\Exception */ public function eloquent(EloquentBuilder $builder): EloquentDataTable { - /** @var string */ + /** @var string $dataTable */ $dataTable = config('datatables.engines.eloquent'); $this->validateDataTable($dataTable, EloquentDataTable::class); @@ -140,11 +126,12 @@ public function eloquent(EloquentBuilder $builder): EloquentDataTable * DataTables using Collection. * * @param \Illuminate\Support\Collection|array $collection - * @return \Yajra\DataTables\CollectionDataTable + * + * @throws \Yajra\DataTables\Exceptions\Exception */ public function collection($collection): CollectionDataTable { - /** @var string */ + /** @var string $dataTable */ $dataTable = config('datatables.engines.collection'); $this->validateDataTable($dataTable, CollectionDataTable::class); @@ -164,44 +151,12 @@ public function resource($resource) } /** - * Get html builder instance. - * - * @return \Yajra\DataTables\Html\Builder - * - * @throws \Yajra\DataTables\Exceptions\Exception - */ - public function getHtmlBuilder() - { - if (! class_exists(Builder::class)) { - throw new Exception('Please install yajra/laravel-datatables-html to be able to use this function.'); - } - - return $this->html ?: $this->html = app('datatables.html'); - } - - /** - * @param string $engine - * @param string $parent - * @return void - * * @throws \Yajra\DataTables\Exceptions\Exception */ public function validateDataTable(string $engine, string $parent): void { if (! ($engine == $parent || is_subclass_of($engine, $parent))) { - $this->throwInvalidEngineException($engine, $parent); + throw new Exception("The given datatable engine `$engine` is not compatible with `$parent`."); } } - - /** - * @param string $engine - * @param string $parent - * @return void - * - * @throws \Yajra\DataTables\Exceptions\Exception - */ - public function throwInvalidEngineException(string $engine, string $parent): void - { - throw new Exception("The given datatable engine `{$engine}` is not compatible with `{$parent}`."); - } } diff --git a/src/DataTablesServiceProvider.php b/src/DataTablesServiceProvider.php index 2b883663..59d29e24 100644 --- a/src/DataTablesServiceProvider.php +++ b/src/DataTablesServiceProvider.php @@ -23,13 +23,9 @@ public function register() $this->setupAssets(); $this->app->alias('datatables', DataTables::class); - $this->app->singleton('datatables', function () { - return new DataTables; - }); + $this->app->singleton('datatables', fn () => new DataTables); - $this->app->singleton('datatables.request', function () { - return new Request; - }); + $this->app->singleton('datatables.request', fn () => new Request); $this->app->singleton('datatables.config', Config::class); } @@ -49,7 +45,7 @@ public function boot() DataTables::macro($engine, function () use ($class) { $canCreate = [$class, 'canCreate']; if (is_callable($canCreate) && ! call_user_func_array($canCreate, func_get_args())) { - throw new \InvalidArgumentException(); + throw new \InvalidArgumentException; } $create = [$class, 'create']; diff --git a/src/EloquentDataTable.php b/src/EloquentDataTable.php index fdd09937..a7783d57 100644 --- a/src/EloquentDataTable.php +++ b/src/EloquentDataTable.php @@ -17,10 +17,14 @@ */ class EloquentDataTable extends QueryDataTable { + /** + * Flag to enable the generation of unique table aliases on eagerly loaded join columns. + * You may want to enable it if you encounter a "Not unique table/alias" error when performing a search or applying ordering. + */ + protected bool $enableEagerJoinAliases = false; + /** * EloquentEngine constructor. - * - * @param Model|EloquentBuilder $model */ public function __construct(Model|EloquentBuilder $model) { @@ -39,7 +43,6 @@ public function __construct(Model|EloquentBuilder $model) * Can the DataTable engine be created with these parameters. * * @param mixed $source - * @return bool */ public static function canCreate($source): bool { @@ -49,7 +52,6 @@ public static function canCreate($source): bool /** * Add columns in collection. * - * @param array $names * @param bool|int $order * @return $this */ @@ -60,9 +62,7 @@ public function addColumns(array $names, $order = false) $name = $attribute; } - $this->addColumn($name, function ($model) use ($attribute) { - return $model->getAttribute($attribute); - }, is_int($order) ? $order++ : $order); + $this->addColumn($name, fn ($model) => $model->getAttribute($attribute), is_int($order) ? $order++ : $order); } return $this; @@ -70,8 +70,6 @@ public function addColumns(array $names, $order = false) /** * If column name could not be resolved then use primary key. - * - * @return string */ protected function getPrimaryKeyName(): string { @@ -79,7 +77,7 @@ protected function getPrimaryKeyName(): string } /** - * @inheritDoc + * {@inheritDoc} */ protected function compileQuerySearch($query, string $column, string $keyword, string $boolean = 'or', bool $nested = false): void { @@ -163,10 +161,7 @@ protected function isMorphRelation($relation) } /** - * Resolve the proper column name be used. - * - * @param string $column - * @return string + * {@inheritDoc} * * @throws \Yajra\DataTables\Exceptions\Exception */ @@ -174,10 +169,10 @@ protected function resolveRelationColumn(string $column): string { $parts = explode('.', $column); $columnName = array_pop($parts); - $relation = implode('.', $parts); + $relation = preg_replace('/\[.*?\]/', '', implode('.', $parts)); if ($this->isNotEagerLoaded($relation)) { - return $column; + return parent::resolveRelationColumn($column); } return $this->joinEagerLoadedColumn($relation, $columnName); @@ -194,64 +189,107 @@ protected function resolveRelationColumn(string $column): string */ protected function joinEagerLoadedColumn($relation, $relationColumn) { - $table = ''; + $tableAlias = $pivotAlias = ''; $lastQuery = $this->query; foreach (explode('.', $relation) as $eachRelation) { $model = $lastQuery->getRelation($eachRelation); + if ($this->enableEagerJoinAliases) { + $lastAlias = $tableAlias ?: $this->getTablePrefix($lastQuery); + $tableAlias = $tableAlias.'_'.$eachRelation; + $pivotAlias = $tableAlias.'_pivot'; + } else { + $lastAlias = $tableAlias ?: $lastQuery->getModel()->getTable(); + } switch (true) { case $model instanceof BelongsToMany: - $pivot = $model->getTable(); - $pivotPK = $model->getExistenceCompareKey(); - $pivotFK = $model->getQualifiedParentKeyName(); + if ($this->enableEagerJoinAliases) { + $pivot = $model->getTable().' as '.$pivotAlias; + } else { + $pivot = $pivotAlias = $model->getTable(); + } + $pivotPK = $pivotAlias.'.'.$model->getForeignPivotKeyName(); + $pivotFK = ltrim($lastAlias.'.'.$model->getParentKeyName(), '.'); $this->performJoin($pivot, $pivotPK, $pivotFK); $related = $model->getRelated(); - $table = $related->getTable(); + if ($this->enableEagerJoinAliases) { + $table = $related->getTable().' as '.$tableAlias; + } else { + $table = $tableAlias = $related->getTable(); + } $tablePK = $model->getRelatedPivotKeyName(); - $foreign = $pivot.'.'.$tablePK; - $other = $related->getQualifiedKeyName(); + $foreign = $pivotAlias.'.'.$tablePK; + $other = $tableAlias.'.'.$related->getKeyName(); - $lastQuery->addSelect($table.'.'.$relationColumn); - $this->performJoin($table, $foreign, $other); + $lastQuery->addSelect($tableAlias.'.'.$relationColumn); break; case $model instanceof HasOneThrough: - $pivot = explode('.', $model->getQualifiedParentKeyName())[0]; // extract pivot table from key - $pivotPK = $pivot.'.'.$model->getFirstKeyName(); - $pivotFK = $model->getQualifiedLocalKeyName(); + if ($this->enableEagerJoinAliases) { + $pivot = explode('.', $model->getQualifiedParentKeyName())[0].' as '.$pivotAlias; + } else { + $pivot = $pivotAlias = explode('.', $model->getQualifiedParentKeyName())[0]; + } + $pivotPK = $pivotAlias.'.'.$model->getFirstKeyName(); + $pivotFK = ltrim($lastAlias.'.'.$model->getLocalKeyName(), '.'); $this->performJoin($pivot, $pivotPK, $pivotFK); $related = $model->getRelated(); - $table = $related->getTable(); + if ($this->enableEagerJoinAliases) { + $table = $related->getTable().' as '.$tableAlias; + } else { + $table = $tableAlias = $related->getTable(); + } $tablePK = $model->getSecondLocalKeyName(); - $foreign = $pivot.'.'.$tablePK; - $other = $related->getQualifiedKeyName(); + $foreign = $pivotAlias.'.'.$tablePK; + $other = $tableAlias.'.'.$related->getKeyName(); $lastQuery->addSelect($lastQuery->getModel()->getTable().'.*'); break; case $model instanceof HasOneOrMany: - $table = $model->getRelated()->getTable(); - $foreign = $model->getQualifiedForeignKeyName(); - $other = $model->getQualifiedParentKeyName(); + if ($this->enableEagerJoinAliases) { + $table = $model->getRelated()->getTable().' as '.$tableAlias; + } else { + $table = $tableAlias = $model->getRelated()->getTable(); + } + $foreign = $tableAlias.'.'.$model->getForeignKeyName(); + $other = ltrim($lastAlias.'.'.$model->getLocalKeyName(), '.'); break; case $model instanceof BelongsTo: - $table = $model->getRelated()->getTable(); - $foreign = $model->getQualifiedForeignKeyName(); - $other = $model->getQualifiedOwnerKeyName(); + if ($this->enableEagerJoinAliases) { + $table = $model->getRelated()->getTable().' as '.$tableAlias; + } else { + $table = $tableAlias = $model->getRelated()->getTable(); + } + $foreign = ltrim($lastAlias.'.'.$model->getForeignKeyName(), '.'); + $other = $tableAlias.'.'.$model->getOwnerKeyName(); break; default: - throw new Exception('Relation '.get_class($model).' is not yet supported.'); + throw new Exception('Relation '.$model::class.' is not yet supported.'); } $this->performJoin($table, $foreign, $other); $lastQuery = $model->getQuery(); } - return $table.'.'.$relationColumn; + return $tableAlias.'.'.$relationColumn; + } + + /** + * Enable the generation of unique table aliases on eagerly loaded join columns. + * You may want to enable it if you encounter a "Not unique table/alias" error when performing a search or applying ordering. + * + * @return $this + */ + public function enableEagerJoinAliases(): static + { + $this->enableEagerJoinAliases = true; + + return $this; } /** @@ -261,12 +299,12 @@ protected function joinEagerLoadedColumn($relation, $relationColumn) * @param string $foreign * @param string $other * @param string $type - * @return void */ protected function performJoin($table, $foreign, $other, $type = 'left'): void { $joins = []; - foreach ((array) $this->getBaseQueryBuilder()->joins as $key => $join) { + $builder = $this->getBaseQueryBuilder(); + foreach ($builder->joins ?? [] as $join) { $joins[] = $join->table; } diff --git a/src/Exceptions/Exception.php b/src/Exceptions/Exception.php index e5e27dca..1f698541 100644 --- a/src/Exceptions/Exception.php +++ b/src/Exceptions/Exception.php @@ -2,6 +2,4 @@ namespace Yajra\DataTables\Exceptions; -class Exception extends \Exception -{ -} +class Exception extends \Exception {} diff --git a/src/Processors/DataProcessor.php b/src/Processors/DataProcessor.php index de7e3f2a..e2ae3087 100644 --- a/src/Processors/DataProcessor.php +++ b/src/Processors/DataProcessor.php @@ -9,13 +9,6 @@ class DataProcessor { - /** - * @var int - */ - protected int $start; - /** - * @var array - */ protected array $output = []; /** @@ -28,14 +21,6 @@ class DataProcessor */ protected array $editColumns = []; - /** - * @var array - */ - protected array $templates = []; - - /** - * @var array - */ protected array $rawColumns = []; /** @@ -43,24 +28,12 @@ class DataProcessor */ protected array $exceptions = ['DT_RowId', 'DT_RowClass', 'DT_RowData', 'DT_RowAttr']; - /** - * @var array - */ protected array $onlyColumns = []; - /** - * @var array - */ protected array $makeHidden = []; - /** - * @var array - */ protected array $makeVisible = []; - /** - * @var array - */ protected array $excessColumns = []; /** @@ -68,30 +41,12 @@ class DataProcessor */ protected mixed $escapeColumns = []; - /** - * @var iterable - */ - protected iterable $results; - - /** - * @var bool - */ protected bool $includeIndex = false; - /** - * @var bool - */ protected bool $ignoreGetters = false; - /** - * @param iterable $results - * @param array $columnDef - * @param array $templates - * @param int $start - */ - public function __construct($results, array $columnDef, array $templates, int $start = 0) + public function __construct(protected iterable $results, array $columnDef, protected array $templates, protected int $start = 0) { - $this->results = $results; $this->appendColumns = $columnDef['append'] ?? []; $this->editColumns = $columnDef['edit'] ?? []; $this->excessColumns = $columnDef['excess'] ?? []; @@ -102,15 +57,12 @@ public function __construct($results, array $columnDef, array $templates, int $s $this->makeHidden = $columnDef['hidden'] ?? []; $this->makeVisible = $columnDef['visible'] ?? []; $this->ignoreGetters = $columnDef['ignore_getters'] ?? false; - $this->templates = $templates; - $this->start = $start; } /** * Process data to output on browser. * * @param bool $object - * @return array */ public function process($object = false): array { @@ -138,9 +90,7 @@ public function process($object = false): array /** * Process add columns. * - * @param array $data * @param array|object|\Illuminate\Database\Eloquent\Model $row - * @return array */ protected function addColumns(array $data, $row): array { @@ -165,10 +115,6 @@ protected function addColumns(array $data, $row): array /** * Process edit columns. - * - * @param array $data - * @param array|object $row - * @return array */ protected function editColumns(array $data, object|array $row): array { @@ -182,10 +128,6 @@ protected function editColumns(array $data, object|array $row): array /** * Setup additional DT row variables. - * - * @param array $data - * @param array|object $row - * @return array */ protected function setupRowVariables(array $data, object|array $row): array { @@ -201,9 +143,6 @@ protected function setupRowVariables(array $data, object|array $row): array /** * Get only needed columns. - * - * @param array $data - * @return array */ protected function selectOnlyNeededColumns(array $data): array { @@ -226,9 +165,6 @@ protected function selectOnlyNeededColumns(array $data): array /** * Remove declared hidden columns. - * - * @param array $data - * @return array */ protected function removeExcessColumns(array $data): array { @@ -241,9 +177,6 @@ protected function removeExcessColumns(array $data): array /** * Flatten array with exceptions. - * - * @param array $array - * @return array */ public function flatten(array $array): array { @@ -261,9 +194,6 @@ public function flatten(array $array): array /** * Escape column values as declared. - * - * @param array $output - * @return array */ protected function escapeColumns(array $output): array { @@ -285,9 +215,6 @@ protected function escapeColumns(array $output): array /** * Escape all string or Htmlable values of row. - * - * @param array $row - * @return array */ protected function escapeRow(array $row): array { diff --git a/src/Processors/RowProcessor.php b/src/Processors/RowProcessor.php index 315b7216..b402b419 100644 --- a/src/Processors/RowProcessor.php +++ b/src/Processors/RowProcessor.php @@ -8,12 +8,9 @@ class RowProcessor { /** - * @param array $data * @param array|object $row */ - public function __construct(protected array $data, protected $row) - { - } + public function __construct(protected array $data, protected $row) {} /** * Process DT RowId and Class value. @@ -21,6 +18,8 @@ public function __construct(protected array $data, protected $row) * @param string $attribute * @param string|callable $template * @return $this + * + * @throws \ReflectionException */ public function rowValue($attribute, $template) { @@ -39,8 +38,9 @@ public function rowValue($attribute, $template) * Process DT Row Data and Attr. * * @param string $attribute - * @param array $template * @return $this + * + * @throws \ReflectionException */ public function rowData($attribute, array $template) { diff --git a/src/QueryDataTable.php b/src/QueryDataTable.php index b4beeafe..01cd5b71 100644 --- a/src/QueryDataTable.php +++ b/src/QueryDataTable.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Query\Expression; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; @@ -15,24 +16,13 @@ class QueryDataTable extends DataTableAbstract { - /** - * Builder object. - * - * @var QueryBuilder - */ - protected QueryBuilder $query; - /** * Flag for ordering NULLS LAST option. - * - * @var bool */ protected bool $nullsLast = false; /** * Flag to check if query preparation was already done. - * - * @var bool */ protected bool $prepared = false; @@ -45,29 +35,21 @@ class QueryDataTable extends DataTableAbstract /** * Flag to keep the select bindings. - * - * @var bool */ protected bool $keepSelectBindings = false; /** * Flag to ignore the selects in count query. - * - * @var bool */ protected bool $ignoreSelectInCountQuery = false; /** * Enable scout search and use this model for searching. - * - * @var Model|null */ protected ?Model $scoutModel = null; /** * Maximum number of hits to return from scout. - * - * @var int */ protected int $scoutMaxHits = 1000; @@ -80,51 +62,43 @@ class QueryDataTable extends DataTableAbstract /** * Flag if scout search was performed. - * - * @var bool */ protected bool $scoutSearched = false; /** * Scout index name. - * - * @var string */ protected string $scoutIndex; /** * Scout key name. - * - * @var string */ protected string $scoutKey; /** * Flag to disable user ordering if a fixed ordering was performed (e.g. scout search). * Only works with corresponding javascript listener. - * - * @var bool */ - protected $disableUserOrdering = false; + protected bool $disableUserOrdering = false; /** - * @param QueryBuilder $builder + * Paginated results. + * + * @var Collection */ - public function __construct(QueryBuilder $builder) + protected Collection $results; + + public function __construct(protected QueryBuilder $query) { - $this->query = $builder; $this->request = app('datatables.request'); $this->config = app('datatables.config'); - $this->columns = $builder->columns; + $this->columns = $this->query->getColumns(); if ($this->config->isDebugging()) { $this->getConnection()->enableQueryLog(); } } - /** - * @return \Illuminate\Database\Connection - */ public function getConnection(): Connection { /** @var Connection $connection */ @@ -137,7 +111,6 @@ public function getConnection(): Connection * Can the DataTable engine be created with these parameters. * * @param mixed $source - * @return bool */ public static function canCreate($source): bool { @@ -147,14 +120,13 @@ public static function canCreate($source): bool /** * Organizes works. * - * @param bool $mDataSupport - * @return \Illuminate\Http\JsonResponse - * * @throws \Exception */ - public function make($mDataSupport = true): JsonResponse + public function make(bool $mDataSupport = true): JsonResponse { try { + $this->validateMinLengthSearch(); + $results = $this->prepareQuery()->results(); $processed = $this->processResults($results, $mDataSupport); $data = $this->transform($results, $processed); @@ -168,11 +140,11 @@ public function make($mDataSupport = true): JsonResponse /** * Get paginated results. * - * @return \Illuminate\Support\Collection + * @return Collection */ public function results(): Collection { - return $this->query->get(); + return $this->results ??= $this->query->get(); } /** @@ -197,8 +169,6 @@ public function prepareQuery(): static /** * Counts current query. - * - * @return int */ public function count(): int { @@ -207,8 +177,6 @@ public function count(): int /** * Prepare count query builder. - * - * @return QueryBuilder */ public function prepareCountQuery(): QueryBuilder { @@ -216,23 +184,28 @@ public function prepareCountQuery(): QueryBuilder if ($this->isComplexQuery($builder)) { $builder->select(DB::raw('1 as dt_row_count')); - if ($this->ignoreSelectInCountQuery || ! $this->isComplexQuery($builder)) { + $clone = $builder->clone(); + $clone->setBindings([]); + if ($clone instanceof EloquentBuilder) { + $clone->getQuery()->wheres = []; + } else { + $clone->wheres = []; + } + + if ($this->isComplexQuery($clone)) { + if (! $this->ignoreSelectInCountQuery) { + $builder = clone $this->query; + } + return $this->getConnection() ->query() ->fromRaw('('.$builder->toSql().') count_row_table') ->setBindings($builder->getBindings()); } - - $builder = clone $this->query; - - return $this->getConnection() - ->query() - ->fromRaw('('.$builder->toSql().') count_row_table') - ->setBindings($builder->getBindings()); } - $row_count = $this->wrap('row_count'); $builder->select($this->getConnection()->raw("'1' as {$row_count}")); + if (! $this->keepSelectBindings) { $builder->setBindings([], 'select'); } @@ -244,7 +217,6 @@ public function prepareCountQuery(): QueryBuilder * Check if builder query uses complex sql. * * @param QueryBuilder|EloquentBuilder $query - * @return bool */ protected function isComplexQuery($query): bool { @@ -253,9 +225,6 @@ protected function isComplexQuery($query): bool /** * Wrap column with DB grammar. - * - * @param string $column - * @return string */ protected function wrap(string $column): string { @@ -276,8 +245,6 @@ public function keepSelectBindings(): static /** * Perform column search. - * - * @return void */ protected function filterRecords(): void { @@ -292,53 +259,162 @@ protected function filterRecords(): void } $this->columnSearch(); + $this->columnControlSearch(); $this->searchPanesSearch(); // If no modification between the original query and the filtered one has been made // the filteredRecords equals the totalRecords - if ($this->query == $initialQuery) { + if (! $this->skipTotalRecords && $this->query == $initialQuery) { $this->filteredRecords ??= $this->totalRecords; } else { $this->filteredCount(); + + if ($this->skipTotalRecords) { + $this->totalRecords = $this->filteredRecords; + } } } /** * Perform column search. - * - * @return void */ public function columnSearch(): void { $columns = $this->request->columns(); foreach ($columns as $index => $column) { - $column = $this->getColumnName($index); + $columnName = $this->getColumnName($index); - if (is_null($column)) { + if (is_null($columnName)) { continue; } - if (! $this->request->isColumnSearchable($index) || $this->isBlacklisted($column) && ! $this->hasFilterColumn($column)) { + if (! $this->request->isColumnSearchable($index) || $this->isBlacklisted($columnName) && ! $this->hasFilterColumn($columnName)) { continue; } - if ($this->hasFilterColumn($column)) { + if ($this->hasFilterColumn($columnName)) { $keyword = $this->getColumnSearchKeyword($index, true); - $this->applyFilterColumn($this->getBaseQueryBuilder(), $column, $keyword); + $this->applyFilterColumn($this->getBaseQueryBuilder(), $columnName, $keyword); } else { - $column = $this->resolveRelationColumn($column); + $columnName = $this->resolveRelationColumn($columnName); $keyword = $this->getColumnSearchKeyword($index); - $this->compileColumnSearch($index, $column, $keyword); + $this->compileColumnSearch($index, $columnName, $keyword); + } + } + } + + public function columnControlSearch(): void + { + $columns = $this->request->columns(); + + foreach ($columns as $index => $column) { + $columnName = $this->getColumnName($index); + + if (is_null($columnName) || ! ($column['searchable'] ?? false)) { + continue; + } + + if ($this->isBlacklisted($columnName) && ! $this->hasFilterColumn($columnName)) { + continue; + } + + $columnControl = $this->request->columnControl($index); + $list = $columnControl['list'] ?? []; + $search = $columnControl['search'] ?? []; + $value = $search['value'] ?? ''; + $logic = $search['logic'] ?? 'equal'; + $mask = $search['mask'] ?? ''; // for date type + $type = $search['type'] ?? 'text'; // text, num, date + + if ($value != '' || str_contains(strtolower($logic), 'empty') || $list) { + $operator = match ($logic) { + 'contains', 'notContains', 'starts', 'ends' => 'LIKE', + 'greater' => '>', + 'less' => '<', + 'greaterOrEqual' => '>=', + 'lessOrEqual' => '<=', + 'empty', 'notEmpty' => null, + default => '=', + }; + + switch ($logic) { + case 'contains': + case 'notContains': + $value = '%'.$value.'%'; + break; + case 'starts': + $value = $value.'%'; + break; + case 'ends': + $value = '%'.$value; + break; + } + + if ($this->hasFilterColumn($columnName)) { + $value = $list ? implode(', ', $list) : $value; + $this->applyFilterColumn($this->getBaseQueryBuilder(), $columnName, $value); + + continue; + } + + // Only resolve relation after checking for a custom filter. + // Because the custom filter for a column might not be found, e.g., $this->hasFilterColumn($columnName) + // and applyFilterColumn() already resolves relations + $columnName = $this->resolveRelationColumn($columnName); + + if ($list) { + if (str_contains($logic, 'not')) { + $this->query->whereNotIn($columnName, $list); + } else { + $this->query->whereIn($columnName, $list); + } + + continue; + } + + if (str_contains(strtolower($logic), 'empty')) { + $this->query->whereNull($columnName, not: $logic === 'notEmpty'); + + continue; + } + + if ($type === 'date') { + try { + // column control replaces / with - on date value + if ($mask && str_contains($mask, '/')) { + $value = str_replace('-', '/', $value); + } + + $value = $mask ? Carbon::createFromFormat($mask, $value) : Carbon::parse($value); + + if ($logic === 'notEqual') { + $this->query->where(function ($q) use ($columnName, $value) { + $q->whereDate($columnName, '!=', $value)->orWhereNull($columnName); + }); + } else { + $this->query->whereDate($columnName, $operator, $value); + } + } catch (\Exception) { + // can't parse date + } + + continue; + } + + if (str_contains($logic, 'not')) { + $this->query->whereNot($columnName, $operator, $value); + + continue; + } + + $this->query->where($columnName, $operator, $value); } } } /** * Check if column has custom filter handler. - * - * @param string $columnName - * @return bool */ public function hasFilterColumn(string $columnName): bool { @@ -347,10 +423,6 @@ public function hasFilterColumn(string $columnName): bool /** * Get column keyword to use for search. - * - * @param int $i - * @param bool $raw - * @return string */ protected function getColumnSearchKeyword(int $i, bool $raw = false): string { @@ -379,10 +451,6 @@ protected function getColumnNameByIndex(int $index): string * Apply filterColumn api search. * * @param QueryBuilder $query - * @param string $columnName - * @param string $keyword - * @param string $boolean - * @return void */ protected function applyFilterColumn($query, string $columnName, string $keyword, string $boolean = 'and'): void { @@ -395,7 +463,7 @@ protected function applyFilterColumn($query, string $columnName, string $keyword $builder = $this->query->newQuery(); } - $callback($builder, $keyword); + $callback($builder, $keyword, fn ($column) => $this->resolveRelationColumn($column)); /** @var \Illuminate\Database\Query\Builder $baseQueryBuilder */ $baseQueryBuilder = $this->getBaseQueryBuilder($builder); @@ -406,9 +474,8 @@ protected function applyFilterColumn($query, string $columnName, string $keyword * Get the base query builder instance. * * @param QueryBuilder|EloquentBuilder|null $instance - * @return QueryBuilder */ - protected function getBaseQueryBuilder($instance = null) + protected function getBaseQueryBuilder($instance = null): QueryBuilder { if (! $instance) { $instance = $this->query; @@ -423,8 +490,6 @@ protected function getBaseQueryBuilder($instance = null) /** * Get query builder instance. - * - * @return QueryBuilder */ public function getQuery(): QueryBuilder { @@ -432,23 +497,15 @@ public function getQuery(): QueryBuilder } /** - * Resolve the proper column name be used. - * - * @param string $column - * @return string + * Resolve the proper column name to be used. */ protected function resolveRelationColumn(string $column): string { - return $column; + return $this->addTablePrefix($this->query, $column); } /** * Compile queries for column search. - * - * @param int $i - * @param string $column - * @param string $keyword - * @return void */ protected function compileColumnSearch(int $i, string $column, string $keyword): void { @@ -461,10 +518,6 @@ protected function compileColumnSearch(int $i, string $column, string $keyword): /** * Compile regex query column search. - * - * @param string $column - * @param string $keyword - * @return void */ protected function regexColumnSearch(string $column, string $keyword): void { @@ -494,34 +547,24 @@ protected function regexColumnSearch(string $column, string $keyword): void /** * Wrap a column and cast based on database driver. - * - * @param string $column - * @return string */ protected function castColumn(string $column): string { - switch ($this->getConnection()->getDriverName()) { - case 'pgsql': - return 'CAST('.$column.' as TEXT)'; - case 'firebird': - return 'CAST('.$column.' as VARCHAR(255))'; - default: - return $column; - } + return match ($this->getConnection()->getDriverName()) { + 'pgsql' => 'CAST('.$column.' as TEXT)', + 'firebird' => 'CAST('.$column.' as VARCHAR(255))', + default => $column, + }; } /** * Compile query builder where clause depending on configurations. * * @param QueryBuilder|EloquentBuilder $query - * @param string $column - * @param string $keyword - * @param string $boolean - * @return void */ protected function compileQuerySearch($query, string $column, string $keyword, string $boolean = 'or'): void { - $column = $this->addTablePrefix($query, $column); + $column = $this->wrap($this->addTablePrefix($query, $column)); $column = $this->castColumn($column); $sql = $column.' LIKE ?'; @@ -537,33 +580,103 @@ protected function compileQuerySearch($query, string $column, string $keyword, s * Ambiguous field error will appear when query use join table and search with keyword. * * @param QueryBuilder|EloquentBuilder $query - * @param string $column - * @return string */ protected function addTablePrefix($query, string $column): string { - if (! str_contains($column, '.')) { - $q = $this->getBaseQueryBuilder($query); - $from = $q->from; - - /** @phpstan-ignore-next-line */ - if (! $from instanceof Expression) { - if (str_contains($from, ' as ')) { - $from = explode(' as ', $from)[1]; - } + // Column is already prefixed + if (str_contains($column, '.')) { + return $column; + } + + // Extract selected columns from the query + $selects = $this->getSelectedColumns($query); + + // We have a match + if (isset($selects['columns'][$column])) { + return $selects['columns'][$column]; + } + + // Multiple wildcards => Unable to determine prefix + if (in_array('*', $selects['wildcards']) || count(array_unique($selects['wildcards'])) > 1) { + return $column; + } + + // Use the only wildcard available + if (! empty($selects['wildcards'])) { + return $selects['wildcards'][0].'.'.$column; + } + + // Fallback on table prefix + return ltrim($this->getTablePrefix($query).'.'.$column, '.'); + } + + /** + * Try to get the base table prefix. + * To be used to prevent ambiguous field name. + * + * @param QueryBuilder|EloquentBuilder $query + */ + protected function getTablePrefix($query): ?string + { + $q = $this->getBaseQueryBuilder($query); + $from = $q->from ?? ''; - $column = $from.'.'.$column; + if (! $from instanceof Expression) { + if (str_contains((string) $from, ' as ')) { + $from = explode(' as ', (string) $from)[1]; } + + return $from; } - return $this->wrap($column); + return null; } /** - * Prepare search keyword based on configurations. + * Get declared column names from the query. * - * @param string $keyword - * @return string + * @param QueryBuilder|EloquentBuilder $query + */ + protected function getSelectedColumns($query): array + { + $q = $this->getBaseQueryBuilder($query); + + $selects = [ + 'wildcards' => [], + 'columns' => [], + ]; + + foreach ($q->columns ?? [] as $select) { + $sql = trim($select instanceof Expression ? $select->getValue($this->getConnection()->getQueryGrammar()) : (string) $select); + // Remove expressions + $sql = preg_replace('/\s*\w*\((?:[^()]*|(?R))*\)/', '_', $sql); + // Remove multiple spaces + $sql = preg_replace('/\s+/', ' ', (string) $sql); + // Remove wrappers + $sql = str_replace(['`', '"', '[', ']'], '', $sql); + // Loop on select columns + foreach (explode(',', $sql) as $column) { + $column = trim($column); + if (preg_match('/[\w.]+\s+(?:as\s+)?([a-zA-Z0-9_]+)$/i', $column, $matches)) { + // Column with alias + $selects['columns'][$matches[1]] = $matches[1]; + } elseif (preg_match('/^([\w.]+)$/i', $column)) { + // Column without alias + [$table, $name] = str_contains($column, '.') ? explode('.', $column) : [null, $column]; + if ($name === '*') { + $selects['wildcards'][] = $table ?? '*'; + } else { + $selects['columns'][$name] = $column; + } + } + } + } + + return $selects; + } + + /** + * Prepare search keyword based on configurations. */ protected function prepareKeyword(string $keyword): string { @@ -590,7 +703,6 @@ protected function prepareKeyword(string $keyword): string * Add custom filter handler for the give column. * * @param string $column - * @param callable $callback * @return $this */ public function filterColumn($column, callable $callback): static @@ -603,7 +715,6 @@ public function filterColumn($column, callable $callback): static /** * Order each given columns versus the given custom sql. * - * @param array $columns * @param string $sql * @param array $bindings * @return $this @@ -625,7 +736,7 @@ public function orderColumns(array $columns, $sql, $bindings = []): static * @param array $bindings * @return $this * - * @internal string $1 Special variable that returns the requested order direction of the column. + * string $1 Special variable that returns the requested order direction of the column. */ public function orderColumn($column, $sql, $bindings = []): static { @@ -648,8 +759,6 @@ public function orderByNullsLast(): static /** * Perform pagination. - * - * @return void */ public function paging(): void { @@ -670,7 +779,6 @@ public function paging(): void * Paginate dataTable using limit without offset * with additional where clause via callback. * - * @param callable $callback * @return $this */ public function limit(callable $callback): static @@ -697,11 +805,6 @@ public function addColumn($name, $content, $order = false): static /** * Perform search using search pane values. - * - * @return void - * - * @throws \Psr\Container\ContainerExceptionInterface - * @throws \Psr\Container\NotFoundExceptionInterface */ protected function searchPanesSearch(): void { @@ -728,13 +831,12 @@ protected function searchPanesSearch(): void */ protected function resolveCallbackParameter(): array { - return [$this->query, $this->scoutSearched]; + return [$this->query, $this->scoutSearched, fn ($column) => $this->resolveRelationColumn($column)]; } /** * Perform default query orderBy clause. * - * @return void * * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface @@ -747,17 +849,12 @@ protected function defaultOrdering(): void return $orderable; }) - ->reject(function ($orderable) { - return $this->isBlacklisted($orderable['name']) && ! $this->hasOrderColumn($orderable['name']); - }) + ->reject(fn ($orderable) => $this->isBlacklisted($orderable['name']) && ! $this->hasOrderColumn($orderable['name'])) ->each(function ($orderable) { - $column = $this->resolveRelationColumn($orderable['name']); - if ($this->hasOrderColumn($orderable['name'])) { - $this->applyOrderColumn($orderable['name'], $orderable); - } elseif ($this->hasOrderColumn($column)) { - $this->applyOrderColumn($column, $orderable); + $this->applyOrderColumn($orderable); } else { + $column = $this->resolveRelationColumn($orderable['name']); $nullsLastSql = $this->getNullsLastSql($column, $orderable['direction']); $normalSql = $this->wrap($column).' '.$orderable['direction']; $sql = $this->nullsLast ? $nullsLastSql : $normalSql; @@ -768,9 +865,6 @@ protected function defaultOrdering(): void /** * Check if column has custom sort handler. - * - * @param string $column - * @return bool */ protected function hasOrderColumn(string $column): bool { @@ -779,22 +873,19 @@ protected function hasOrderColumn(string $column): bool /** * Apply orderColumn custom query. - * - * @param string $column - * @param array $orderable */ - protected function applyOrderColumn(string $column, array $orderable): void + protected function applyOrderColumn(array $orderable): void { - $sql = $this->columnDef['order'][$column]['sql']; + $sql = $this->columnDef['order'][$orderable['name']]['sql']; if ($sql === false) { return; } if (is_callable($sql)) { - call_user_func($sql, $this->query, $orderable['direction']); + call_user_func($sql, $this->query, $orderable['direction'], fn ($column) => $this->resolveRelationColumn($column)); } else { - $sql = str_replace('$1', $orderable['direction'], $sql); - $bindings = $this->columnDef['order'][$column]['bindings']; + $sql = str_replace('$1', $orderable['direction'], (string) $sql); + $bindings = $this->columnDef['order'][$orderable['name']]['bindings']; $this->query->orderByRaw($sql, $bindings); } } @@ -804,7 +895,6 @@ protected function applyOrderColumn(string $column, array $orderable): void * * @param string $column * @param string $direction - * @return string * * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface @@ -823,9 +913,6 @@ protected function getNullsLastSql($column, $direction): string /** * Perform global search for the given keyword. - * - * @param string $keyword - * @return void */ protected function globalSearch(string $keyword): void { @@ -836,13 +923,9 @@ protected function globalSearch(string $keyword): void $this->query->where(function ($query) use ($keyword) { collect($this->request->searchableColumnIndex()) - ->map(function ($index) { - return $this->getColumnName($index); - }) + ->map(fn ($index) => $this->getColumnName($index)) ->filter() - ->reject(function ($column) { - return $this->isBlacklisted($column) && ! $this->hasFilterColumn($column); - }) + ->reject(fn ($column) => $this->isBlacklisted($column) && ! $this->hasFilterColumn($column)) ->each(function ($column) use ($keyword, $query) { if ($this->hasFilterColumn($column)) { $this->applyFilterColumn($query, $column, $keyword, 'or'); @@ -858,7 +941,6 @@ protected function globalSearch(string $keyword): void * individual words and searches for each of them. * * @param string $keyword - * @return void */ protected function smartGlobalSearch($keyword): void { @@ -872,9 +954,6 @@ protected function smartGlobalSearch($keyword): void /** * Append debug parameters on output. - * - * @param array $output - * @return array */ protected function showDebugger(array $output): array { @@ -893,9 +972,6 @@ protected function showDebugger(array $output): array /** * Attach custom with meta on response. - * - * @param array $data - * @return array */ protected function attachAppends(array $data): array { @@ -916,8 +992,6 @@ protected function attachAppends(array $data): array /** * Get filtered, ordered and paginated query. - * - * @return QueryBuilder */ public function getFilteredQuery(): QueryBuilder { @@ -940,8 +1014,6 @@ public function ignoreSelectsInCountQuery(): static /** * Perform sorting of columns. - * - * @return void */ public function ordering(): void { @@ -957,8 +1029,6 @@ public function ordering(): void * Enable scout search and use provided model for searching. * $max_hits is the maximum number of hits to return from scout. * - * @param string $model - * @param int $max_hits * @return $this * * @throws \Exception @@ -984,7 +1054,6 @@ public function enableScoutSearch(string $model, int $max_hits = 1000): static /** * Add dynamic filters to scout search. * - * @param callable $callback * @return $this */ public function scoutFilter(callable $callback): static @@ -996,9 +1065,6 @@ public function scoutFilter(callable $callback): static /** * Apply scout search to query if enabled. - * - * @param string $search_keyword - * @return bool */ protected function applyScoutSearch(string $search_keyword): bool { @@ -1040,8 +1106,6 @@ protected function applyScoutSearch(string $search_keyword): bool * * Currently supported drivers: MySQL * - * @param string $keyName - * @param array $orderedKeys * @return bool */ protected function applyFixedOrderingToQuery(string $keyName, array $orderedKeys) @@ -1052,11 +1116,10 @@ protected function applyFixedOrderingToQuery(string $keyName, array $orderedKeys // Escape keyName and orderedKeys $keyName = $connection->getQueryGrammar()->wrap($keyName); $orderedKeys = collect($orderedKeys) - ->map(function ($value) use ($connection) { - return $connection->escape($value); - }); + ->map(fn ($value) => $connection->escape($value)); switch ($driverName) { + case 'mariadb': case 'mysql': $this->query->orderByRaw("FIELD($keyName, ".$orderedKeys->implode(',').')'); @@ -1098,15 +1161,12 @@ protected function applyFixedOrderingToQuery(string $keyName, array $orderedKeys /** * Perform a scout search with the configured engine and given parameters. Return matching model IDs. * - * @param string $searchKeyword - * @param mixed $searchFilters - * @return array * * @throws \Exception */ protected function performScoutSearch(string $searchKeyword, mixed $searchFilters = []): array { - if (! class_exists('\Laravel\Scout\EngineManager')) { + if (! class_exists(\Laravel\Scout\EngineManager::class)) { throw new \Exception('Laravel Scout is not installed.'); } $engine = app(\Laravel\Scout\EngineManager::class)->engine(); diff --git a/src/Utilities/Config.php b/src/Utilities/Config.php index 99a2aa98..ae474368 100644 --- a/src/Utilities/Config.php +++ b/src/Utilities/Config.php @@ -6,25 +6,13 @@ class Config { - /** - * @var \Illuminate\Contracts\Config\Repository - */ - private Repository $repository; - /** * Config constructor. - * - * @param \Illuminate\Contracts\Config\Repository $repository */ - public function __construct(Repository $repository) - { - $this->repository = $repository; - } + public function __construct(private readonly Repository $repository) {} /** * Check if config uses wild card search. - * - * @return bool */ public function isWildcard(): bool { @@ -33,8 +21,6 @@ public function isWildcard(): bool /** * Check if config uses smart search. - * - * @return bool */ public function isSmartSearch(): bool { @@ -43,8 +29,6 @@ public function isSmartSearch(): bool /** * Check if config uses case-insensitive search. - * - * @return bool */ public function isCaseInsensitive(): bool { @@ -53,8 +37,6 @@ public function isCaseInsensitive(): bool /** * Check if app is in debug mode. - * - * @return bool */ public function isDebugging(): bool { @@ -65,10 +47,9 @@ public function isDebugging(): bool * Get the specified configuration value. * * @param string $key - * @param mixed $default * @return mixed */ - public function get($key, $default = null) + public function get($key, mixed $default = null) { return $this->repository->get($key, $default); } @@ -77,18 +58,15 @@ public function get($key, $default = null) * Set a given configuration value. * * @param array|string $key - * @param mixed $value * @return void */ - public function set($key, $value = null) + public function set($key, mixed $value = null) { $this->repository->set($key, $value); } /** * Check if dataTable config uses multi-term searching. - * - * @return bool */ public function isMultiTerm(): bool { @@ -97,8 +75,6 @@ public function isMultiTerm(): bool /** * Check if dataTable config uses starts_with searching. - * - * @return bool */ public function isStartsWithSearch(): bool { diff --git a/src/Utilities/Helper.php b/src/Utilities/Helper.php index c3f5108a..b0e87127 100644 --- a/src/Utilities/Helper.php +++ b/src/Utilities/Helper.php @@ -6,6 +6,7 @@ use DateTime; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Blade; use Illuminate\Support\Str; use ReflectionFunction; use ReflectionMethod; @@ -14,12 +15,8 @@ class Helper { /** * Places item of extra columns into results by care of their order. - * - * @param array $item - * @param array $array - * @return array */ - public static function includeInArray($item, $array) + public static function includeInArray(array $item, array $array): array { if (self::isItemOrderInvalid($item, $array)) { return array_merge($array, [$item['name'] => $item['content']]); @@ -44,12 +41,8 @@ public static function includeInArray($item, $array) /** * Check if item order is valid. - * - * @param array $item - * @param array $array - * @return bool */ - protected static function isItemOrderInvalid($item, $array) + protected static function isItemOrderInvalid(array $item, array $array): bool { return $item['order'] === false || $item['order'] >= count($array); } @@ -94,7 +87,7 @@ private static function reflectCallableParameters($callable) * * @throws \ReflectionException */ - public static function compileContent($content, array $data, array|object $param) + public static function compileContent(mixed $content, array $data, array|object $param) { if (is_string($content)) { return static::compileBlade($content, static::getMixedValue($data, $param)); @@ -122,33 +115,23 @@ public static function compileContent($content, array $data, array|object $param /** * Parses and compiles strings by using Blade Template System. * - * @param string $str - * @param array $data - * @return false|string + * + * @throws \Throwable */ - public static function compileBlade($str, $data = []) + public static function compileBlade(string $str, array $data = []): false|string { if (view()->exists($str)) { /** @var view-string $str */ return view($str, $data)->render(); } - ob_start() && extract($data, EXTR_SKIP); - eval('?>'.app('blade.compiler')->compileString($str)); - $str = ob_get_contents(); - ob_end_clean(); - - return $str; + return Blade::render($str, $data); } /** * Get a mixed value of custom data and the parameters. - * - * @param array $data - * @param array|object $param - * @return array */ - public static function getMixedValue(array $data, array|object $param) + public static function getMixedValue(array $data, array|object $param): array { $casted = self::castToArray($param); @@ -165,9 +148,6 @@ public static function getMixedValue(array $data, array|object $param) /** * Cast the parameter into an array. - * - * @param array|object $param - * @return array */ public static function castToArray(array|object $param): array { @@ -180,11 +160,8 @@ public static function castToArray(array|object $param): array /** * Get equivalent or method of query builder. - * - * @param string $method - * @return string */ - public static function getOrMethod($method) + public static function getOrMethod(string $method): string { if (! Str::contains(Str::lower($method), 'or')) { return 'or'.ucfirst($method); @@ -195,12 +172,8 @@ public static function getOrMethod($method) /** * Converts array object values to associative array. - * - * @param mixed $row - * @param array $filters - * @return array */ - public static function convertToArray($row, $filters = []) + public static function convertToArray(mixed $row, array $filters = []): array { if (Arr::get($filters, 'ignore_getters') && is_object($row) && method_exists($row, 'getAttributes')) { $data = $row->getAttributes(); @@ -226,7 +199,7 @@ public static function convertToArray($row, $filters = []) $data = $row instanceof Arrayable ? $row->toArray() : (array) $row; foreach ($data as &$value) { - if (is_object($value) || is_array($value)) { + if ((is_object($value) && ! $value instanceof DateTime) || is_array($value)) { $value = self::convertToArray($value); } @@ -236,24 +209,17 @@ public static function convertToArray($row, $filters = []) return $data; } - /** - * @param array $data - * @return array - */ - public static function transform(array $data) + public static function transform(array $data): array { - return array_map(function ($row) { - return self::transformRow($row); - }, $data); + return array_map(fn ($row) => self::transformRow($row), $data); } /** * Transform row data into an array. * * @param array $row - * @return array */ - protected static function transformRow($row) + protected static function transformRow($row): array { foreach ($row as $key => $value) { if ($value instanceof DateTime) { @@ -272,11 +238,8 @@ protected static function transformRow($row) /** * Build parameters depending on # of arguments passed. - * - * @param array $args - * @return array */ - public static function buildParameters(array $args) + public static function buildParameters(array $args): array { $parameters = []; @@ -296,20 +259,15 @@ public static function buildParameters(array $args) /** * Replace all pattern occurrences with keyword. - * - * @param array $subject - * @param string $keyword - * @param string $pattern - * @return array */ - public static function replacePatternWithKeyword(array $subject, $keyword, $pattern = '$1') + public static function replacePatternWithKeyword(array $subject, string $keyword, string $pattern = '$1'): array { $parameters = []; foreach ($subject as $param) { if (is_array($param)) { $parameters[] = self::replacePatternWithKeyword($param, $keyword, $pattern); } else { - $parameters[] = str_replace($pattern, $keyword, $param); + $parameters[] = str_replace($pattern, $keyword, (string) $param); } } @@ -318,12 +276,8 @@ public static function replacePatternWithKeyword(array $subject, $keyword, $patt /** * Get column name from string. - * - * @param string $str - * @param bool $wantsAlias - * @return string */ - public static function extractColumnName($str, $wantsAlias) + public static function extractColumnName(string $str, bool $wantsAlias): string { $matches = explode(' as ', Str::lower($str)); @@ -344,25 +298,16 @@ public static function extractColumnName($str, $wantsAlias) /** * Adds % wildcards to the given string. - * - * @param string $str - * @param bool $lowercase - * @return string */ - public static function wildcardLikeString($str, $lowercase = true) + public static function wildcardLikeString(string $str, bool $lowercase = true): string { return static::wildcardString($str, '%', $lowercase); } /** * Adds wildcards to the given string. - * - * @param string $str - * @param string $wildcard - * @param bool $lowercase - * @return string */ - public static function wildcardString($str, $wildcard, $lowercase = true) + public static function wildcardString(string $str, string $wildcard, bool $lowercase = true): string { $wild = $wildcard; $chars = (array) preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY); @@ -387,7 +332,7 @@ public static function toJsonScript(array $parameters, int $options = 0): string foreach (Arr::dot($parameters) as $key => $value) { if (self::isJavascript($value, $key)) { - $values[] = trim($value); + $values[] = trim((string) $value); Arr::set($parameters, $key, '%'.$key.'%'); $replacements[] = '"%'.$key.'%"'; } diff --git a/src/Utilities/Request.php b/src/Utilities/Request.php index f660e08c..c26d9102 100644 --- a/src/Utilities/Request.php +++ b/src/Utilities/Request.php @@ -9,19 +9,6 @@ */ class Request { - /** - * @var BaseRequest - */ - protected BaseRequest $request; - - /** - * Request constructor. - */ - public function __construct() - { - $this->request = app('request'); - } - /** * Proxy non-existing method calls to base request class. * @@ -31,12 +18,22 @@ public function __construct() */ public function __call($name, $arguments) { - $callback = [$this->request, $name]; + $callback = [request(), $name]; if (is_callable($callback)) { return call_user_func_array($callback, $arguments); } } + /** + * Determine if an attribute exists on the base request. + * + * @param string $name + */ + public function __isset($name): bool + { + return isset(request()->$name); + } + /** * Get attributes from request instance. * @@ -45,44 +42,35 @@ public function __call($name, $arguments) */ public function __get($name) { - return $this->request->__get($name); + return request()->__get($name); } /** * Get all columns request input. - * - * @return array */ public function columns(): array { - return (array) $this->request->input('columns'); + return (array) request()->input('columns'); } /** * Check if DataTables is searchable. - * - * @return bool */ public function isSearchable(): bool { - return $this->request->input('search.value') != ''; + return request()->input('search.value') != ''; } /** * Check if DataTables must uses regular expressions. - * - * @param int $index - * @return bool */ public function isRegex(int $index): bool { - return $this->request->input("columns.$index.search.regex") === 'true'; + return request()->input("columns.$index.search.regex") === 'true'; } /** * Get orderable columns. - * - * @return array */ public function orderableColumns(): array { @@ -91,14 +79,14 @@ public function orderableColumns(): array } $orderable = []; - for ($i = 0, $c = count((array) $this->request->input('order')); $i < $c; $i++) { + for ($i = 0, $c = count((array) request()->input('order')); $i < $c; $i++) { /** @var int $order_col */ - $order_col = $this->request->input("order.$i.column"); + $order_col = request()->input("order.$i.column"); /** @var string $direction */ - $direction = $this->request->input("order.$i.dir"); + $direction = request()->input("order.$i.dir"); - $order_dir = strtolower($direction) === 'asc' ? 'asc' : 'desc'; + $order_dir = $direction && strtolower($direction) === 'asc' ? 'asc' : 'desc'; if ($this->isColumnOrderable($order_col)) { $orderable[] = ['column' => $order_col, 'direction' => $order_dir]; } @@ -109,23 +97,18 @@ public function orderableColumns(): array /** * Check if DataTables ordering is enabled. - * - * @return bool */ public function isOrderable(): bool { - return $this->request->input('order') && count((array) $this->request->input('order')) > 0; + return request()->input('order') && count((array) request()->input('order')) > 0; } /** * Check if a column is orderable. - * - * @param int $index - * @return bool */ public function isColumnOrderable(int $index): bool { - return $this->request->input("columns.$index.orderable", 'true') == 'true'; + return request()->input("columns.$index.orderable", 'true') == 'true'; } /** @@ -136,7 +119,7 @@ public function isColumnOrderable(int $index): bool public function searchableColumnIndex() { $searchable = []; - $columns = (array) $this->request->input('columns'); + $columns = (array) request()->input('columns'); for ($i = 0, $c = count($columns); $i < $c; $i++) { if ($this->isColumnSearchable($i, false)) { $searchable[] = $i; @@ -148,48 +131,48 @@ public function searchableColumnIndex() /** * Check if a column is searchable. - * - * @param int $i - * @param bool $column_search - * @return bool */ public function isColumnSearchable(int $i, bool $column_search = true): bool { if ($column_search) { return ( - $this->request->input("columns.$i.searchable", 'true') === 'true' + request()->input("columns.$i.searchable", 'true') === 'true' || - $this->request->input("columns.$i.searchable", 'true') === true + request()->input("columns.$i.searchable", 'true') === true ) && $this->columnKeyword($i) != ''; } return - $this->request->input("columns.$i.searchable", 'true') === 'true' + request()->input("columns.$i.searchable", 'true') === 'true' || - $this->request->input("columns.$i.searchable", 'true') === true; + request()->input("columns.$i.searchable", 'true') === true; } /** * Get column's search value. - * - * @param int $index - * @return string */ public function columnKeyword(int $index): string { /** @var string $keyword */ - $keyword = $this->request->input("columns.$index.search.value") ?? ''; + $keyword = request()->input("columns.$index.search.value") ?? ''; return $this->prepareKeyword($keyword); } + public function columnControl(int $index): array + { + return request()->array("columns.$index.columnControl"); + } + + public function columnControlSearch(int $index): array + { + return request()->array("columns.$index.columnControl.search"); + } + /** * Prepare keyword string value. - * - * @param float|array|int|string $keyword - * @return string */ protected function prepareKeyword(float|array|int|string $keyword): string { @@ -202,83 +185,67 @@ protected function prepareKeyword(float|array|int|string $keyword): string /** * Get global search keyword. - * - * @return string */ public function keyword(): string { /** @var string $keyword */ - $keyword = $this->request->input('search.value') ?? ''; + $keyword = request()->input('search.value') ?? ''; return $this->prepareKeyword($keyword); } /** * Get column name by index. - * - * @param int $i - * @return string|null */ public function columnName(int $i): ?string { /** @var string[] $column */ - $column = $this->request->input("columns.$i"); + $column = request()->input("columns.$i"); return (isset($column['name']) && $column['name'] != '') ? $column['name'] : $column['data']; } /** * Check if DataTables allow pagination. - * - * @return bool */ public function isPaginationable(): bool { - return ! is_null($this->request->input('start')) && - ! is_null($this->request->input('length')) && - $this->request->input('length') != -1; + return ! is_null(request()->input('start')) && + ! is_null(request()->input('length')) && + request()->input('length') != -1; } - /** - * @return BaseRequest - */ public function getBaseRequest(): BaseRequest { - return $this->request; + return request(); } /** * Get starting record value. - * - * @return int */ public function start(): int { - $start = $this->request->input('start', 0); + $start = request()->input('start', 0); return is_numeric($start) ? intval($start) : 0; } /** * Get per page length. - * - * @return int */ public function length(): int { - $length = $this->request->input('length', 10); + $length = request()->input('length', 10); return is_numeric($length) ? intval($length) : 10; } /** * Get draw request. - * - * @return int */ public function draw(): int { - $draw = $this->request->input('draw', 0); + $draw = request()->input('draw', 0); return is_numeric($draw) ? intval($draw) : 0; } diff --git a/src/config/datatables.php b/src/config/datatables.php index 08912643..bdb963fe 100644 --- a/src/config/datatables.php +++ b/src/config/datatables.php @@ -42,7 +42,7 @@ /* * List of available builders for DataTables. - * This is where you can register your custom dataTables builder. + * This is where you can register your custom DataTables builder. */ 'engines' => [ 'eloquent' => Yajra\DataTables\EloquentDataTable::class, @@ -57,10 +57,10 @@ * Note, only change this if you know what you are doing! */ 'builders' => [ - //Illuminate\Database\Eloquent\Relations\Relation::class => 'eloquent', - //Illuminate\Database\Eloquent\Builder::class => 'eloquent', - //Illuminate\Database\Query\Builder::class => 'query', - //Illuminate\Support\Collection::class => 'collection', + // Illuminate\Database\Eloquent\Relations\Relation::class => 'eloquent', + // Illuminate\Database\Eloquent\Builder::class => 'eloquent', + // Illuminate\Database\Query\Builder::class => 'query', + // Illuminate\Support\Collection::class => 'collection', ], /* @@ -79,7 +79,7 @@ 'error' => env('DATATABLES_ERROR', null), /* - * Default columns definition of dataTable utility functions. + * Default columns definition of DataTable utility functions. */ 'columns' => [ /* @@ -105,7 +105,7 @@ 'blacklist' => ['password', 'remember_token'], /* - * List of columns that are only allowed fo search/sort. + * List of columns that are only allowed for search/sort. * If set to *, all columns are allowed. */ 'whitelist' => '*', diff --git a/src/helper.php b/src/helper.php index ec17d6f1..7aa58a1a 100644 --- a/src/helper.php +++ b/src/helper.php @@ -6,7 +6,7 @@ * Or return the factory if source is not set. * * @param \Illuminate\Contracts\Database\Query\Builder|\Illuminate\Contracts\Database\Eloquent\Builder|\Illuminate\Support\Collection|array|null $source - * @return \Yajra\DataTables\DataTables|\Yajra\DataTables\DataTableAbstract + * @return ($source is null ? \Yajra\DataTables\DataTables : \Yajra\DataTables\DataTableAbstract) * * @throws \Yajra\DataTables\Exceptions\Exception */ diff --git a/src/lumen.php b/src/lumen.php index a8167231..bfb2d376 100644 --- a/src/lumen.php +++ b/src/lumen.php @@ -22,6 +22,6 @@ function config_path($path = '') */ function public_path($path = null) { - return rtrim(app()->basePath('public/'.$path), '/'); + return rtrim((string) app()->basePath('public/'.$path), '/'); } } diff --git a/tests/Formatters/DateFormatter.php b/tests/Formatters/DateFormatter.php index f4bc82b1..1a4e0abe 100644 --- a/tests/Formatters/DateFormatter.php +++ b/tests/Formatters/DateFormatter.php @@ -8,12 +8,7 @@ class DateFormatter implements Formatter { - public string $format; - - public function __construct(string $format = 'Y-m-d h:i a') - { - $this->format = $format; - } + public function __construct(public string $format = 'Y-m-d h:i a') {} public function format($value, $row): string { diff --git a/tests/Integration/BelongsToManyRelationTest.php b/tests/Integration/BelongsToManyRelationTest.php index 4f2ad21f..c11a4517 100644 --- a/tests/Integration/BelongsToManyRelationTest.php +++ b/tests/Integration/BelongsToManyRelationTest.php @@ -3,6 +3,7 @@ namespace Yajra\DataTables\Tests\Integration; use Illuminate\Foundation\Testing\DatabaseTransactions; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\DataTables; use Yajra\DataTables\Tests\Models\User; use Yajra\DataTables\Tests\TestCase; @@ -11,7 +12,7 @@ class BelongsToManyRelationTest extends TestCase { use DatabaseTransactions; - /** @test */ + #[Test] public function it_returns_all_records_with_the_relation_when_called_without_parameters() { $response = $this->call('GET', '/relations/belongsToMany'); @@ -25,7 +26,7 @@ public function it_returns_all_records_with_the_relation_when_called_without_par $this->assertCount(20, $response->json()['data']); } - /** @test */ + #[Test] public function it_can_perform_global_search_on_the_relation() { $response = $this->getJsonResponse([ @@ -54,7 +55,7 @@ protected function getJsonResponse(array $params = []) return $this->call('GET', '/relations/belongsToMany', array_merge($data, $params)); } - /** @test */ + #[Test] public function it_can_sort_using_the_relation_with_pagination() { $response = $this->getJsonResponse([ @@ -86,8 +87,6 @@ protected function setUp(): void { parent::setUp(); - $this->app['router']->get('/relations/belongsToMany', function (DataTables $datatables) { - return $datatables->eloquent(User::with('roles')->select('users.*'))->toJson(); - }); + $this->app['router']->get('/relations/belongsToMany', fn (DataTables $datatables) => $datatables->eloquent(User::with('roles')->select('users.*'))->toJson()); } } diff --git a/tests/Integration/BelongsToRelationTest.php b/tests/Integration/BelongsToRelationTest.php index 424ccac5..ff962dcd 100644 --- a/tests/Integration/BelongsToRelationTest.php +++ b/tests/Integration/BelongsToRelationTest.php @@ -3,6 +3,7 @@ namespace Yajra\DataTables\Tests\Integration; use Illuminate\Foundation\Testing\DatabaseTransactions; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\DataTables; use Yajra\DataTables\Tests\Models\Post; use Yajra\DataTables\Tests\TestCase; @@ -11,7 +12,7 @@ class BelongsToRelationTest extends TestCase { use DatabaseTransactions; - /** @test */ + #[Test] public function it_returns_all_records_with_the_relation_when_called_without_parameters() { $response = $this->call('GET', '/relations/belongsTo'); @@ -25,7 +26,7 @@ public function it_returns_all_records_with_the_relation_when_called_without_par $this->assertCount(60, $response->json()['data']); } - /** @test */ + #[Test] public function it_can_perform_global_search_on_the_relation() { $response = $this->getJsonResponse([ @@ -54,7 +55,7 @@ protected function getJsonResponse(array $params = []) return $this->call('GET', '/relations/belongsTo', array_merge($data, $params)); } - /** @test */ + #[Test] public function it_can_sort_using_the_relation_with_pagination() { $response = $this->getJsonResponse([ @@ -83,8 +84,6 @@ protected function setUp(): void { parent::setUp(); - $this->app['router']->get('/relations/belongsTo', function (DataTables $datatables) { - return $datatables->eloquent(Post::with('user')->select('posts.*'))->toJson(); - }); + $this->app['router']->get('/relations/belongsTo', fn (DataTables $datatables) => $datatables->eloquent(Post::with('user')->select('posts.*'))->toJson()); } } diff --git a/tests/Integration/CollectionDataTableTest.php b/tests/Integration/CollectionDataTableTest.php index 1538f7a5..6ce01576 100644 --- a/tests/Integration/CollectionDataTableTest.php +++ b/tests/Integration/CollectionDataTableTest.php @@ -4,6 +4,7 @@ use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Http\JsonResponse; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\CollectionDataTable; use Yajra\DataTables\DataTables; use Yajra\DataTables\Facades\DataTables as DatatablesFacade; @@ -14,7 +15,7 @@ class CollectionDataTableTest extends TestCase { use DatabaseTransactions; - /** @test */ + #[Test] public function it_returns_all_records_when_no_parameters_is_passed() { $crawler = $this->call('GET', '/collection/users'); @@ -25,7 +26,7 @@ public function it_returns_all_records_when_no_parameters_is_passed() ]); } - /** @test */ + #[Test] public function it_returns_zero_filtered_records_on_empty_collection() { $crawler = $this->call('GET', '/collection/empty'); @@ -38,7 +39,7 @@ public function it_returns_zero_filtered_records_on_empty_collection() ]); } - /** @test */ + #[Test] public function it_can_perform_global_search() { $crawler = $this->call('GET', '/collection/users', [ @@ -56,7 +57,7 @@ public function it_can_perform_global_search() ]); } - /** @test */ + #[Test] public function it_accepts_a_model_collection_using_of_factory() { $dataTable = DataTables::of(User::all()); @@ -65,7 +66,7 @@ public function it_accepts_a_model_collection_using_of_factory() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_accepts_a_collection_using_of_factory() { $dataTable = DataTables::of(collect()); @@ -74,7 +75,7 @@ public function it_accepts_a_collection_using_of_factory() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_accepts_a_model_collection_using_facade() { $dataTable = DatatablesFacade::of(User::all()); @@ -83,7 +84,7 @@ public function it_accepts_a_model_collection_using_facade() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_accepts_a_collection_using_facade() { $dataTable = DatatablesFacade::of(collect()); @@ -92,7 +93,7 @@ public function it_accepts_a_collection_using_facade() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_accepts_a_model_using_ioc_container() { $dataTable = app('datatables')->collection(User::all()); @@ -101,7 +102,7 @@ public function it_accepts_a_model_using_ioc_container() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_can_sort_case_insensitive_strings() { config()->set('app.debug', false); @@ -143,7 +144,7 @@ public function it_can_sort_case_insensitive_strings() ], $response->getData(true)); } - /** @test */ + #[Test] public function it_can_sort_numeric_strings() { config()->set('app.debug', false); @@ -185,7 +186,7 @@ public function it_can_sort_numeric_strings() ], $response->getData(true)); } - /** @test */ + #[Test] public function it_accepts_a_model_using_ioc_container_factory() { $dataTable = app('datatables')->of(User::all()); @@ -194,7 +195,7 @@ public function it_accepts_a_model_using_ioc_container_factory() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_can_search_on_added_columns() { config()->set('app.debug', false); @@ -235,7 +236,7 @@ public function it_can_search_on_added_columns() ], $response->getData(true)); } - /** @test */ + #[Test] public function it_accepts_array_data_source() { $source = [ @@ -252,12 +253,8 @@ protected function setUp(): void { parent::setUp(); - $this->app['router']->get('/collection/users', function (DataTables $datatables) { - return $datatables->collection(User::all())->toJson(); - }); + $this->app['router']->get('/collection/users', fn (DataTables $datatables) => $datatables->collection(User::all())->toJson()); - $this->app['router']->get('/collection/empty', function (DataTables $datatables) { - return $datatables->collection([])->toJson(); - }); + $this->app['router']->get('/collection/empty', fn (DataTables $datatables) => $datatables->collection([])->toJson()); } } diff --git a/tests/Integration/CustomOrderTest.php b/tests/Integration/CustomOrderTest.php index 467e8e4d..f7281f3e 100644 --- a/tests/Integration/CustomOrderTest.php +++ b/tests/Integration/CustomOrderTest.php @@ -3,6 +3,7 @@ namespace Yajra\DataTables\Tests\Integration; use Illuminate\Foundation\Testing\DatabaseTransactions; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\DataTables; use Yajra\DataTables\Tests\Models\Post; use Yajra\DataTables\Tests\TestCase; @@ -11,7 +12,7 @@ class CustomOrderTest extends TestCase { use DatabaseTransactions; - /** @test */ + #[Test] public function it_can_order_with_custom_order() { $response = $this->getJsonResponse([ @@ -52,12 +53,10 @@ protected function setUp(): void { parent::setUp(); - $this->app['router']->get('/relations/belongsTo', function (DataTables $datatables) { - return $datatables->eloquent(Post::with('user')->select('posts.*')) - ->orderColumn('user.id', function ($query, $order) { - $query->orderBy('users.id', $order == 'desc' ? 'asc' : 'desc'); - }) - ->toJson(); - }); + $this->app['router']->get('/relations/belongsTo', fn (DataTables $datatables) => $datatables->eloquent(Post::with('user')->select('posts.*')) + ->orderColumn('user.id', function ($query, $order, $resolver) { + $query->orderBy($resolver('user.id'), $order == 'desc' ? 'asc' : 'desc'); + }) + ->toJson()); } } diff --git a/tests/Integration/DeepRelationTest.php b/tests/Integration/DeepRelationTest.php index 9ebe9578..4ad9c861 100644 --- a/tests/Integration/DeepRelationTest.php +++ b/tests/Integration/DeepRelationTest.php @@ -3,6 +3,7 @@ namespace Yajra\DataTables\Tests\Integration; use Illuminate\Foundation\Testing\DatabaseTransactions; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\DataTables; use Yajra\DataTables\Tests\Models\Post; use Yajra\DataTables\Tests\TestCase; @@ -11,7 +12,7 @@ class DeepRelationTest extends TestCase { use DatabaseTransactions; - /** @test */ + #[Test] public function it_returns_all_records_with_the_relation_when_called_without_parameters() { $response = $this->getJsonResponse(); @@ -24,7 +25,7 @@ public function it_returns_all_records_with_the_relation_when_called_without_par $this->assertCount(60, $response->json()['data']); } - /** @test */ + #[Test] public function it_can_perform_global_search_on_the_relation() { $response = $this->getJsonResponse([ diff --git a/tests/Integration/EloquentDataTableTest.php b/tests/Integration/EloquentDataTableTest.php index afaa5560..12664742 100644 --- a/tests/Integration/EloquentDataTableTest.php +++ b/tests/Integration/EloquentDataTableTest.php @@ -5,6 +5,7 @@ use Carbon\Carbon; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Http\JsonResponse; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\DataTables; use Yajra\DataTables\EloquentDataTable; use Yajra\DataTables\Facades\DataTables as DatatablesFacade; @@ -17,7 +18,7 @@ class EloquentDataTableTest extends TestCase { use DatabaseTransactions; - /** @test */ + #[Test] public function it_returns_all_records_when_no_parameters_is_passed() { $crawler = $this->call('GET', '/eloquent/users'); @@ -28,7 +29,7 @@ public function it_returns_all_records_when_no_parameters_is_passed() ]); } - /** @test */ + #[Test] public function it_can_perform_global_search() { $crawler = $this->call('GET', '/eloquent/users', [ @@ -46,7 +47,7 @@ public function it_can_perform_global_search() ]); } - /** @test */ + #[Test] public function it_accepts_a_model_using_of_factory() { $dataTable = DataTables::of(User::query()); @@ -55,7 +56,7 @@ public function it_accepts_a_model_using_of_factory() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_accepts_a_model_using_facade() { $dataTable = DatatablesFacade::of(User::query()); @@ -64,7 +65,7 @@ public function it_accepts_a_model_using_facade() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_accepts_a_model_using_facade_eloquent_method() { $dataTable = DatatablesFacade::eloquent(User::query()); @@ -73,7 +74,7 @@ public function it_accepts_a_model_using_facade_eloquent_method() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_accepts_a_model_using_ioc_container() { $dataTable = app('datatables')->eloquent(User::query()); @@ -82,7 +83,7 @@ public function it_accepts_a_model_using_ioc_container() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_accepts_a_model_using_ioc_container_factory() { $dataTable = app('datatables')->of(User::query()); @@ -91,7 +92,7 @@ public function it_accepts_a_model_using_ioc_container_factory() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_returns_only_the_selected_columns_with_dotted_notation() { $json = $this->call('GET', '/eloquent/only')->json(); @@ -101,7 +102,7 @@ public function it_returns_only_the_selected_columns_with_dotted_notation() $this->assertArrayHasKey('name', $json['data'][0]['user']); } - /** @test */ + #[Test] public function it_can_return_formatted_columns() { $crawler = $this->call('GET', '/eloquent/formatColumn'); @@ -121,7 +122,7 @@ public function it_can_return_formatted_columns() $this->assertEquals(Carbon::parse($user->created_at)->format('Y-m-d'), $data['created_at_formatted']); } - /** @test */ + #[Test] public function it_can_return_formatted_column_using_closure() { $crawler = $this->call('GET', '/eloquent/formatColumn-closure'); @@ -141,7 +142,7 @@ public function it_can_return_formatted_column_using_closure() $this->assertEquals(Carbon::parse($user->created_at)->format('Y-m-d'), $data['created_at_formatted']); } - /** @test */ + #[Test] public function it_can_return_formatted_column_on_invalid_formatter() { $crawler = $this->call('GET', '/eloquent/formatColumn-fallback'); @@ -161,7 +162,7 @@ public function it_can_return_formatted_column_on_invalid_formatter() $this->assertEquals($user->created_at, $data['created_at_formatted']); } - /** @test */ + #[Test] public function it_accepts_a_relation() { $user = User::first(); @@ -177,32 +178,22 @@ protected function setUp(): void parent::setUp(); $router = $this->app['router']; - $router->get('/eloquent/users', function (DataTables $datatables) { - return $datatables->eloquent(User::query())->toJson(); - }); - - $router->get('/eloquent/only', function (DataTables $datatables) { - return $datatables->eloquent(Post::with('user')) - ->only(['title', 'user.name']) - ->toJson(); - }); - - $router->get('/eloquent/formatColumn', function (DataTables $dataTable) { - return $dataTable->eloquent(User::query()) - ->formatColumn('created_at', new DateFormatter('Y-m-d')) - ->toJson(); - }); - - $router->get('/eloquent/formatColumn-closure', function (DataTables $dataTable) { - return $dataTable->eloquent(User::query()) - ->formatColumn('created_at', fn ($value, $row) => Carbon::parse($value)->format('Y-m-d')) - ->toJson(); - }); - - $router->get('/eloquent/formatColumn-fallback', function (DataTables $dataTable) { - return $dataTable->eloquent(User::query()) - ->formatColumn('created_at', 'InvalidFormatter::class') - ->toJson(); - }); + $router->get('/eloquent/users', fn (DataTables $datatables) => $datatables->eloquent(User::query())->toJson()); + + $router->get('/eloquent/only', fn (DataTables $datatables) => $datatables->eloquent(Post::with('user')) + ->only(['title', 'user.name']) + ->toJson()); + + $router->get('/eloquent/formatColumn', fn (DataTables $dataTable) => $dataTable->eloquent(User::query()) + ->formatColumn('created_at', new DateFormatter('Y-m-d')) + ->toJson()); + + $router->get('/eloquent/formatColumn-closure', fn (DataTables $dataTable) => $dataTable->eloquent(User::query()) + ->formatColumn('created_at', fn ($value, $row) => Carbon::parse($value)->format('Y-m-d')) + ->toJson()); + + $router->get('/eloquent/formatColumn-fallback', fn (DataTables $dataTable) => $dataTable->eloquent(User::query()) + ->formatColumn('created_at', 'InvalidFormatter::class') + ->toJson()); } } diff --git a/tests/Integration/EloquentJoinTest.php b/tests/Integration/EloquentJoinTest.php index e0bfaed5..4ec67b76 100644 --- a/tests/Integration/EloquentJoinTest.php +++ b/tests/Integration/EloquentJoinTest.php @@ -3,6 +3,7 @@ namespace Yajra\DataTables\Tests\Integration; use Illuminate\Foundation\Testing\DatabaseTransactions; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\DataTables; use Yajra\DataTables\Tests\Models\Post; use Yajra\DataTables\Tests\TestCase; @@ -11,7 +12,7 @@ class EloquentJoinTest extends TestCase { use DatabaseTransactions; - /** @test */ + #[Test] public function it_returns_all_records_with_the_relation_when_called_without_parameters() { $response = $this->getJsonResponse(); @@ -27,7 +28,7 @@ public function it_returns_all_records_with_the_relation_when_called_without_par $this->assertCount(60, $response->json()['data']); } - /** @test */ + #[Test] public function it_can_perform_global_search_on_the_relation() { $response = $this->getJsonResponse([ @@ -56,7 +57,7 @@ protected function getJsonResponse(array $params = []) return $this->call('GET', '/eloquent/join', array_merge($data, $params)); } - /** @test */ + #[Test] public function it_can_sort_using_the_relation_with_pagination() { $response = $this->getJsonResponse([ @@ -87,8 +88,8 @@ protected function setUp(): void $this->app['router']->get('/eloquent/join', function (DataTables $datatables) { $builder = Post::query() - ->join('users', 'users.id', '=', 'posts.user_id') - ->select('users.name', 'users.email', 'posts.title'); + ->join('users', 'users.id', '=', 'posts.user_id') + ->select('users.name', 'users.email', 'posts.title'); return $datatables->eloquent($builder)->toJson(); }); diff --git a/tests/Integration/HasManyRelationTest.php b/tests/Integration/HasManyRelationTest.php index 661bb8cf..41e1cff6 100644 --- a/tests/Integration/HasManyRelationTest.php +++ b/tests/Integration/HasManyRelationTest.php @@ -3,6 +3,7 @@ namespace Yajra\DataTables\Tests\Integration; use Illuminate\Foundation\Testing\DatabaseTransactions; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\DataTables; use Yajra\DataTables\Tests\Models\Post; use Yajra\DataTables\Tests\Models\User; @@ -12,7 +13,7 @@ class HasManyRelationTest extends TestCase { use DatabaseTransactions; - /** @test */ + #[Test] public function it_returns_all_records_with_the_relation_when_called_without_parameters() { $response = $this->call('GET', '/relations/hasMany'); @@ -26,7 +27,7 @@ public function it_returns_all_records_with_the_relation_when_called_without_par $this->assertCount(20, $response->json()['data']); } - /** @test */ + #[Test] public function it_returns_all_records_with_deleted_relations_when_called_with_withtrashed_parameter() { Post::find(1)->delete(); @@ -42,7 +43,7 @@ public function it_returns_all_records_with_deleted_relations_when_called_with_w $this->assertCount(3, $response->json()['data'][0]['posts']); } - /** @test */ + #[Test] public function it_returns_all_records_with_only_deleted_relations_when_called_with_onlytrashed_parameter() { Post::find(1)->delete(); @@ -57,7 +58,7 @@ public function it_returns_all_records_with_only_deleted_relations_when_called_w $this->assertCount(1, $response->json()['data'][0]['posts']); } - /** @test */ + #[Test] public function it_can_perform_global_search_on_the_relation() { $response = $this->getJsonResponse([ @@ -89,20 +90,14 @@ protected function setUp(): void { parent::setUp(); - $this->app['router']->get('/relations/hasMany', function (DataTables $datatables) { - return $datatables->eloquent(User::with('posts')->select('users.*'))->toJson(); - }); - - $this->app['router']->get('/relations/hasManyWithTrashed', function (DataTables $datatables) { - return $datatables->eloquent(User::with(['posts' => function ($query) { - $query->withTrashed(); - }])->select('users.*'))->toJson(); - }); - - $this->app['router']->get('/relations/hasManyOnlyTrashed', function (DataTables $datatables) { - return $datatables->eloquent(User::with(['posts' => function ($query) { - $query->onlyTrashed(); - }])->select('users.*'))->toJson(); - }); + $this->app['router']->get('/relations/hasMany', fn (DataTables $datatables) => $datatables->eloquent(User::with('posts')->select('users.*'))->toJson()); + + $this->app['router']->get('/relations/hasManyWithTrashed', fn (DataTables $datatables) => $datatables->eloquent(User::with(['posts' => function ($query) { + $query->withTrashed(); + }])->select('users.*'))->toJson()); + + $this->app['router']->get('/relations/hasManyOnlyTrashed', fn (DataTables $datatables) => $datatables->eloquent(User::with(['posts' => function ($query) { + $query->onlyTrashed(); + }])->select('users.*'))->toJson()); } } diff --git a/tests/Integration/HasOneRelationTest.php b/tests/Integration/HasOneRelationTest.php index e1ceb416..da83b66e 100644 --- a/tests/Integration/HasOneRelationTest.php +++ b/tests/Integration/HasOneRelationTest.php @@ -3,6 +3,7 @@ namespace Yajra\DataTables\Tests\Integration; use Illuminate\Foundation\Testing\DatabaseTransactions; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\DataTables; use Yajra\DataTables\Tests\Models\Heart; use Yajra\DataTables\Tests\Models\User; @@ -12,7 +13,7 @@ class HasOneRelationTest extends TestCase { use DatabaseTransactions; - /** @test */ + #[Test] public function it_returns_all_records_with_the_relation_when_called_without_parameters() { $response = $this->call('GET', '/relations/hasOne'); @@ -26,7 +27,7 @@ public function it_returns_all_records_with_the_relation_when_called_without_par $this->assertCount(20, $response->json()['data']); } - /** @test */ + #[Test] public function it_returns_all_records_with_the_deleted_relation_when_called_with_withtrashed_parameter() { Heart::find(1)->delete(); @@ -44,7 +45,7 @@ public function it_returns_all_records_with_the_deleted_relation_when_called_wit $this->assertNotEmpty($response->json()['data'][1]['heart']); } - /** @test */ + #[Test] public function it_returns_all_records_with_the_only_deleted_relation_when_called_with_onlytrashed_parameter() { Heart::find(1)->delete(); @@ -62,7 +63,7 @@ public function it_returns_all_records_with_the_only_deleted_relation_when_calle $this->assertEmpty($response->json()['data'][1]['heart']); } - /** @test */ + #[Test] public function it_can_perform_global_search_on_the_relation() { $response = $this->getJsonResponse([ @@ -91,7 +92,7 @@ protected function getJsonResponse(array $params = []) return $this->call('GET', '/relations/hasOne', array_merge($data, $params)); } - /** @test */ + #[Test] public function it_can_sort_using_the_relation_with_pagination() { $response = $this->getJsonResponse([ @@ -120,20 +121,18 @@ protected function setUp(): void { parent::setUp(); - $this->app['router']->get('/relations/hasOne', function (DataTables $datatables) { - return $datatables->eloquent(User::with('heart')->select('users.*'))->toJson(); - }); + $this->app['router']->get('/relations/hasOne', fn (DataTables $datatables) => $datatables->eloquent(User::with('heart')->select('users.*'))->toJson()); - $this->app['router']->get('/relations/hasOneWithTrashed', function (DataTables $datatables) { - return $datatables->eloquent(User::with(['heart' => function ($query) { + $this->app['router']->get('/relations/hasOneWithTrashed', fn (DataTables $datatables) => $datatables->eloquent(User::with([ + 'heart' => function ($query) { $query->withTrashed(); - }])->select('users.*'))->toJson(); - }); + }, + ])->select('users.*'))->toJson()); - $this->app['router']->get('/relations/hasOneOnlyTrashed', function (DataTables $datatables) { - return $datatables->eloquent(User::with(['heart' => function ($query) { + $this->app['router']->get('/relations/hasOneOnlyTrashed', fn (DataTables $datatables) => $datatables->eloquent(User::with([ + 'heart' => function ($query) { $query->onlyTrashed(); - }])->select('users.*'))->toJson(); - }); + }, + ])->select('users.*'))->toJson()); } } diff --git a/tests/Integration/HasOneThroughTest.php b/tests/Integration/HasOneThroughTest.php index f7347945..075de4c1 100644 --- a/tests/Integration/HasOneThroughTest.php +++ b/tests/Integration/HasOneThroughTest.php @@ -3,6 +3,7 @@ namespace Yajra\DataTables\Tests\Integration; use Illuminate\Foundation\Testing\DatabaseTransactions; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\DataTables; use Yajra\DataTables\Tests\Models\Heart; use Yajra\DataTables\Tests\Models\Post; @@ -12,7 +13,7 @@ class HasOneThroughTest extends TestCase { use DatabaseTransactions; - /** @test */ + #[Test] public function it_returns_all_records_with_the_relation_when_called_without_parameters() { $response = $this->call('GET', '/relations/hasOneThrough'); @@ -26,7 +27,7 @@ public function it_returns_all_records_with_the_relation_when_called_without_par $this->assertCount(60, $response->json()['data']); } - /** @test */ + #[Test] public function it_can_search_has_one_through_relation() { $response = $this->call('GET', '/relations/hasOneThroughSearchRelation', [ @@ -51,7 +52,7 @@ public function it_can_search_has_one_through_relation() $this->assertCount(33, $response->json()['data']); } - /** @test */ + #[Test] public function it_returns_all_records_with_the_deleted_relation_when_called_with_withtrashed_parameter() { Heart::find(1)->delete(); @@ -69,7 +70,7 @@ public function it_returns_all_records_with_the_deleted_relation_when_called_wit $this->assertNotEmpty($response->json()['data'][1]['heart']); } - /** @test */ + #[Test] public function it_returns_all_records_with_the_only_deleted_relation_when_called_with_onlytrashed_parameter() { Heart::find(1)->delete(); @@ -90,7 +91,7 @@ public function it_returns_all_records_with_the_only_deleted_relation_when_calle $this->assertEmpty($response->json()['data'][3]['heart']); } - /** @test */ + #[Test] public function it_can_perform_global_search_on_the_relation() { $response = $this->getJsonResponse([ @@ -122,24 +123,16 @@ protected function setUp(): void { parent::setUp(); - $this->app['router']->get('/relations/hasOneThrough', function (DataTables $datatables) { - return $datatables->eloquent(Post::with('heart')->select('posts.*'))->toJson(); - }); - - $this->app['router']->get('/relations/hasOneThroughSearchRelation', function (DataTables $datatables) { - return $datatables->eloquent(Post::with('heart'))->addColumns(['hearts.size'])->toJson(); - }); - - $this->app['router']->get('/relations/hasOneThroughWithTrashed', function (DataTables $datatables) { - return $datatables->eloquent(Post::with(['heart' => function ($query) { - $query->withTrashed(); - }])->select('posts.*'))->toJson(); - }); - - $this->app['router']->get('/relations/hasOneThroughOnlyTrashed', function (DataTables $datatables) { - return $datatables->eloquent(Post::with(['heart' => function ($query) { - $query->onlyTrashed(); - }])->select('posts.*'))->toJson(); - }); + $this->app['router']->get('/relations/hasOneThrough', fn (DataTables $datatables) => $datatables->eloquent(Post::with('heart')->select('posts.*'))->toJson()); + + $this->app['router']->get('/relations/hasOneThroughSearchRelation', fn (DataTables $datatables) => $datatables->eloquent(Post::with('heart'))->addColumns(['hearts.size'])->toJson()); + + $this->app['router']->get('/relations/hasOneThroughWithTrashed', fn (DataTables $datatables) => $datatables->eloquent(Post::with(['heart' => function ($query) { + $query->withTrashed(); + }])->select('posts.*'))->toJson()); + + $this->app['router']->get('/relations/hasOneThroughOnlyTrashed', fn (DataTables $datatables) => $datatables->eloquent(Post::with(['heart' => function ($query) { + $query->onlyTrashed(); + }])->select('posts.*'))->toJson()); } } diff --git a/tests/Integration/IgnoreGettersTest.php b/tests/Integration/IgnoreGettersTest.php index 2db7c2a7..18068b9c 100644 --- a/tests/Integration/IgnoreGettersTest.php +++ b/tests/Integration/IgnoreGettersTest.php @@ -3,6 +3,7 @@ namespace Yajra\DataTables\Tests\Integration; use Illuminate\Foundation\Testing\DatabaseTransactions; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\DataTables; use Yajra\DataTables\Tests\Models\User; use Yajra\DataTables\Tests\TestCase; @@ -11,7 +12,7 @@ class IgnoreGettersTest extends TestCase { use DatabaseTransactions; - /** @test */ + #[Test] public function it_return_the_default_value_when_attribute_is_null() { $user = User::create([ @@ -24,12 +25,10 @@ public function it_return_the_default_value_when_attribute_is_null() $this->assertEquals('#000000', $user->refresh()->toArray()['color']); } - /** @test */ + #[Test] public function it_return_the_getter_value_without_ignore_getters() { - $this->app['router']->get('/ignore-getters', function (DataTables $datatables) { - return $datatables->eloquent(User::with('posts.user')->select('users.*'))->toJson(); - }); + $this->app['router']->get('/ignore-getters', fn (DataTables $datatables) => $datatables->eloquent(User::with('posts.user')->select('users.*'))->toJson()); $response = $this->call('GET', '/ignore-getters'); $response->assertJson([ @@ -46,12 +45,10 @@ public function it_return_the_getter_value_without_ignore_getters() $this->assertCount(20, $response->json()['data']); } - /** @test */ + #[Test] public function it_ignore_the_getter_value_with_ignore_getters() { - $this->app['router']->get('/ignore-getters', function (DataTables $datatables) { - return $datatables->eloquent(User::with('posts.user')->select('users.*'))->ignoreGetters()->toJson(); - }); + $this->app['router']->get('/ignore-getters', fn (DataTables $datatables) => $datatables->eloquent(User::with('posts.user')->select('users.*'))->ignoreGetters()->toJson()); $response = $this->call('GET', '/ignore-getters'); $response->assertJson([ diff --git a/tests/Integration/MinSearchLengthDataTableTest.php b/tests/Integration/MinSearchLengthDataTableTest.php new file mode 100644 index 00000000..fcab93d4 --- /dev/null +++ b/tests/Integration/MinSearchLengthDataTableTest.php @@ -0,0 +1,99 @@ +call('GET', '/eloquent/min-length', [ + 'start' => 0, + 'length' => 10, + 'columns' => [ + ['data' => 'id'], + ['data' => 'name'], + ['data' => 'email'], + ], + 'search' => [ + 'value' => '', + 'regex' => false, + ], + ]); + + $crawler->assertJson([ + 'draw' => 0, + 'recordsTotal' => 20, + 'recordsFiltered' => 20, + ]); + } + + #[Test] + public function it_returns_an_error_when_search_keyword_length_is_less_than_required() + { + $crawler = $this->call('GET', '/eloquent/min-length', [ + 'start' => 0, + 'length' => 10, + 'columns' => [ + ['data' => 'id'], + ['data' => 'name'], + ['data' => 'email'], + ], + 'search' => [ + 'value' => 'abc', + 'regex' => false, + ], + ]); + + $crawler->assertJson([ + 'draw' => 0, + 'recordsTotal' => 0, + 'recordsFiltered' => 0, + 'data' => [], + 'error' => "Exception Message:\n\nPlease enter at least 5 characters to search.", + ]); + } + + #[Test] + public function it_returns_filtered_records_when_search_keyword_length_is_met() + { + $crawler = $this->call('GET', '/eloquent/min-length', [ + 'draw' => 1, + 'start' => 0, + 'length' => 10, + 'columns' => [ + ['data' => 'id'], + ['data' => 'name'], + ['data' => 'email'], + ], + 'search' => [ + 'value' => 'Record-17', + 'regex' => false, + ], + ]); + + $crawler->assertJson([ + 'draw' => 1, + 'recordsTotal' => 20, + 'recordsFiltered' => 1, + ]); + } + + protected function setUp(): void + { + parent::setUp(); + + Route::get('/eloquent/min-length', fn () => (new EloquentDataTable(User::query())) + ->minSearchLength(5) + ->toJson()); + } +} diff --git a/tests/Integration/MorphToRelationTest.php b/tests/Integration/MorphToRelationTest.php index 51ca65fd..44f07140 100644 --- a/tests/Integration/MorphToRelationTest.php +++ b/tests/Integration/MorphToRelationTest.php @@ -3,6 +3,7 @@ namespace Yajra\DataTables\Tests\Integration; use Illuminate\Foundation\Testing\DatabaseTransactions; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\DataTables; use Yajra\DataTables\Tests\Models\HumanUser; use Yajra\DataTables\Tests\Models\User; @@ -15,7 +16,7 @@ class MorphToRelationTest extends TestCase { use DatabaseTransactions; - /** @test */ + #[Test] public function it_returns_all_records_with_the_relation_when_called_without_parameters() { $response = $this->call('GET', '/relations/morphTo'); @@ -29,7 +30,7 @@ public function it_returns_all_records_with_the_relation_when_called_without_par $this->assertCount(20, $response->json()['data']); } - /** @test */ + #[Test] public function it_returns_all_records_with_the_deleted_relation_when_called_with_withtrashed_parameter() { HumanUser::find(1)->delete(); @@ -47,7 +48,7 @@ public function it_returns_all_records_with_the_deleted_relation_when_called_wit $this->assertNotEmpty($response->json()['data'][1]['user']); } - /** @test */ + #[Test] public function it_returns_all_records_with_the_only_deleted_relation_when_called_with_onlytrashed_parameter() { HumanUser::find(1)->delete(); @@ -65,7 +66,7 @@ public function it_returns_all_records_with_the_only_deleted_relation_when_calle $this->assertEmpty($response->json()['data'][1]['user']); } - /** @test */ + #[Test] public function it_can_perform_global_search_on_the_relation() { $response = $this->getJsonResponse([ @@ -98,20 +99,18 @@ protected function setUp(): void { parent::setUp(); - $this->app['router']->get('/relations/morphTo', function (DataTables $datatables) { - return $datatables->eloquent(User::with('user')->select('users.*'))->toJson(); - }); + $this->app['router']->get('/relations/morphTo', fn (DataTables $datatables) => $datatables->eloquent(User::with('user')->select('users.*'))->toJson()); - $this->app['router']->get('/relations/morphToWithTrashed', function (DataTables $datatables) { - return $datatables->eloquent(User::with(['user' => function ($query) { + $this->app['router']->get('/relations/morphToWithTrashed', fn (DataTables $datatables) => $datatables->eloquent(User::with([ + 'user' => function ($query) { $query->withTrashed(); - }])->select('users.*'))->toJson(); - }); + }, + ])->select('users.*'))->toJson()); - $this->app['router']->get('/relations/morphToOnlyTrashed', function (DataTables $datatables) { - return $datatables->eloquent(User::with(['user' => function ($query) { + $this->app['router']->get('/relations/morphToOnlyTrashed', fn (DataTables $datatables) => $datatables->eloquent(User::with([ + 'user' => function ($query) { $query->onlyTrashed(); - }])->select('users.*'))->toJson(); - }); + }, + ])->select('users.*'))->toJson()); } } diff --git a/tests/Integration/QueryDataTableTest.php b/tests/Integration/QueryDataTableTest.php index 14aa280b..c6477f35 100644 --- a/tests/Integration/QueryDataTableTest.php +++ b/tests/Integration/QueryDataTableTest.php @@ -7,6 +7,8 @@ use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\DB; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestWith; use Yajra\DataTables\DataTables; use Yajra\DataTables\Facades\DataTables as DatatablesFacade; use Yajra\DataTables\QueryDataTable; @@ -18,7 +20,7 @@ class QueryDataTableTest extends TestCase { use DatabaseTransactions; - /** @test */ + #[Test] public function it_can_set_total_records() { $crawler = $this->call('GET', '/set-total-records'); @@ -29,7 +31,7 @@ public function it_can_set_total_records() ]); } - /** @test */ + #[Test] public function it_can_set_zero_total_records() { $crawler = $this->call('GET', '/zero-total-records'); @@ -40,7 +42,7 @@ public function it_can_set_zero_total_records() ]); } - /** @test */ + #[Test] public function it_can_set_total_filtered_records() { $crawler = $this->call('GET', '/set-filtered-records'); @@ -51,18 +53,25 @@ public function it_can_set_total_filtered_records() ]); } - /** @test */ + #[Test] public function it_returns_all_records_when_no_parameters_is_passed() { + DB::enableQueryLog(); + $crawler = $this->call('GET', '/query/users'); $crawler->assertJson([ 'draw' => 0, 'recordsTotal' => 20, 'recordsFiltered' => 20, ]); + + DB::disableQueryLog(); + $queryLog = DB::getQueryLog(); + + $this->assertCount(2, $queryLog); } - /** @test */ + #[Test] public function it_can_perform_global_search() { $crawler = $this->call('GET', '/query/users', [ @@ -80,10 +89,30 @@ public function it_can_perform_global_search() ]); } - /** @test */ + #[Test] public function it_can_skip_total_records_count_query() { - $crawler = $this->call('GET', '/query/simple', [ + DB::enableQueryLog(); + + $crawler = $this->call('GET', '/skip-total-records'); + $crawler->assertJson([ + 'draw' => 0, + 'recordsTotal' => 20, + 'recordsFiltered' => 20, + ]); + + DB::disableQueryLog(); + $queryLog = DB::getQueryLog(); + + $this->assertCount(2, $queryLog); + } + + #[Test] + public function it_can_skip_total_records_count_query_with_filter_applied() + { + DB::enableQueryLog(); + + $crawler = $this->call('GET', '/skip-total-records', [ 'columns' => [ ['data' => 'name', 'name' => 'name', 'searchable' => 'true', 'orderable' => 'true'], ['data' => 'email', 'name' => 'email', 'searchable' => 'true', 'orderable' => 'true'], @@ -93,12 +122,17 @@ public function it_can_skip_total_records_count_query() $crawler->assertJson([ 'draw' => 0, - 'recordsTotal' => 0, + 'recordsTotal' => 1, 'recordsFiltered' => 1, ]); + + DB::disableQueryLog(); + $queryLog = DB::getQueryLog(); + + $this->assertCount(2, $queryLog); } - /** @test */ + #[Test] public function it_can_perform_multiple_term_global_search() { $crawler = $this->call('GET', '/query/users', [ @@ -116,7 +150,7 @@ public function it_can_perform_multiple_term_global_search() ]); } - /** @test */ + #[Test] public function it_accepts_a_query_using_of_factory() { $dataTable = DataTables::of(DB::table('users')); @@ -125,7 +159,7 @@ public function it_accepts_a_query_using_of_factory() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_accepts_a_query_using_facade() { $dataTable = DatatablesFacade::of(DB::table('users')); @@ -134,7 +168,7 @@ public function it_accepts_a_query_using_facade() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_accepts_a_query_using_facade_query_method() { $dataTable = DatatablesFacade::query(DB::table('users')); @@ -143,7 +177,7 @@ public function it_accepts_a_query_using_facade_query_method() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_accepts_a_query_using_ioc_container() { $dataTable = app('datatables')->query(DB::table('users')); @@ -152,7 +186,7 @@ public function it_accepts_a_query_using_ioc_container() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_accepts_a_query_using_ioc_container_factory() { $dataTable = app('datatables')->of(DB::table('users')); @@ -161,7 +195,7 @@ public function it_accepts_a_query_using_ioc_container_factory() $this->assertInstanceOf(JsonResponse::class, $response); } - /** @test */ + #[Test] public function it_does_not_allow_search_on_added_columns() { $crawler = $this->call('GET', '/query/addColumn', [ @@ -180,7 +214,7 @@ public function it_does_not_allow_search_on_added_columns() ]); } - /** @test */ + #[Test] public function it_returns_only_the_selected_columns() { $json = $this->call('GET', '/query/only')->json(); @@ -188,8 +222,8 @@ public function it_returns_only_the_selected_columns() $this->assertArrayHasKey('name', $json['data'][0]); } - /** @test */ - public function it_edit_only_the_selected_columns_after_using_editOnlySelectedColumns() + #[Test] + public function it_edit_only_the_selected_columns_after_using_edit_only_selected_columns() { $json = $this->call('GET', '/query/edit-columns', [ 'columns' => [ @@ -202,7 +236,7 @@ public function it_edit_only_the_selected_columns_after_using_editOnlySelectedCo $this->assertNotEquals('edited', $json['data'][0]['email']); } - /** @test */ + #[Test] public function it_does_not_allow_raw_html_on_added_columns() { $json = $this->call('GET', '/query/xss-add')->json(); @@ -210,7 +244,7 @@ public function it_does_not_allow_raw_html_on_added_columns() $this->assertNotEquals('Allowed', $json['data'][0]['bar']); } - /** @test */ + #[Test] public function it_does_not_allow_raw_html_on_edited_columns() { $json = $this->call('GET', '/query/xss-edit')->json(); @@ -218,7 +252,7 @@ public function it_does_not_allow_raw_html_on_edited_columns() $this->assertNotEquals('Allowed', $json['data'][0]['email']); } - /** @test */ + #[Test] public function it_allows_raw_html_on_specified_columns() { $json = $this->call('GET', '/query/xss-raw')->json(); @@ -227,7 +261,7 @@ public function it_allows_raw_html_on_specified_columns() $this->assertEquals('Allowed', $json['data'][0]['email']); } - /** @test */ + #[Test] public function it_can_return_auto_index_column() { $crawler = $this->call('GET', '/query/indexColumn', [ @@ -248,7 +282,7 @@ public function it_can_return_auto_index_column() $this->assertArrayHasKey('DT_RowIndex', $crawler->json()['data'][0]); } - /** @test */ + #[Test] public function it_allows_search_on_added_column_with_custom_filter_handler() { $crawler = $this->call('GET', '/query/filterColumn', [ @@ -270,7 +304,7 @@ public function it_allows_search_on_added_column_with_custom_filter_handler() $this->assertStringContainsString('"1" = ?', $queries[1]['query']); } - /** @test */ + #[Test] public function it_returns_search_panes_options() { $crawler = $this->call('GET', '/query/search-panes'); @@ -291,7 +325,7 @@ public function it_returns_search_panes_options() $this->assertEquals(count($options['id']), 20); } - /** @test */ + #[Test] public function it_performs_search_using_search_panes() { $crawler = $this->call('GET', '/query/search-panes', [ @@ -307,7 +341,7 @@ public function it_performs_search_using_search_panes() ]); } - /** @test */ + #[Test] public function it_allows_column_search_added_column_with_custom_filter_handler() { $crawler = $this->call('GET', '/query/blacklisted-filter', [ @@ -332,7 +366,7 @@ public function it_allows_column_search_added_column_with_custom_filter_handler( ]); } - /** @test */ + #[Test] public function it_can_return_formatted_columns() { $crawler = $this->call('GET', '/query/formatColumn'); @@ -353,7 +387,7 @@ public function it_can_return_formatted_columns() $this->assertEquals(Carbon::parse($user->created_at)->format('Y-m-d'), $data['created_at_formatted']); } - /** @test */ + #[Test] public function it_can_return_added_column_with_dependency_injection() { $crawler = $this->call('GET', '/closure-di'); @@ -373,140 +407,150 @@ public function it_can_return_added_column_with_dependency_injection() $this->assertEquals($user->name.'_di', $data['name_di']); } - protected function setUp(): void + #[Test] + #[TestWith(['title', '"posts"."title"'])] // column from base table wildcard posts.* + #[TestWith(['email', '"users"."email"'])] // column from join table added to select without alias + #[TestWith(['alias_field_with_as', '"alias_field_with_as"'])] + #[TestWith(['alias_field_without_as', '"alias_field_without_as"'])] + #[TestWith(['alias_expression_with_as', '"alias_expression_with_as"'])] + #[TestWith(['alias_expression_without_as', '"alias_expression_without_as"'])] + #[TestWith(['alias_case_with_as', '"alias_case_with_as"'])] + #[TestWith(['alias_case_without_as', '"alias_case_without_as"'])] + #[TestWith(['sub_query', '"sub_query"'])] + public function it_can_detect_column_alias(string $column, string $expected) { - parent::setUp(); - - $router = $this->app['router']; + DB::enableQueryLog(); - $router->get('/query/users', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users'))->toJson(); - }); - - $router->get('/query/formatColumn', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->formatColumn('created_at', new DateFormatter('Y-m-d')) - ->toJson(); - }); - - $router->get('/query/simple', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users'))->skipTotalRecords()->toJson(); - }); - - $router->get('/query/addColumn', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->addColumn('foo', 'bar') - ->toJson(); - }); - - $router->get('/query/indexColumn', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->addIndexColumn() - ->toJson(); - }); + $crawler = $this->call('GET', '/query/aliases', [ + 'columns' => [ + ['data' => $column, 'name' => $column, 'orderable' => 'true'], + ], + 'order' => [['column' => 0, 'dir' => 'asc']], + ]); - $router->get('/query/filterColumn', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->addColumn('foo', 'bar') - ->filterColumn('foo', function (Builder $builder, $keyword) { - $builder->where('1', $keyword); - }) - ->toJson(); - }); + $crawler->assertJsonStructure([ + 'draw', + 'recordsTotal', + 'recordsFiltered', + ]); - $router->get('/query/blacklisted-filter', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->addColumn('foo', 'bar') - ->filterColumn('foo', function (Builder $builder, $keyword) { - $builder->where('name', $keyword); - }) - ->blacklist(['foo']) - ->toJson(); - }); + DB::disableQueryLog(); + $queryLog = DB::getQueryLog(); - $router->get('/query/only', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->addColumn('foo', 'bar') - ->only(['name']) - ->toJson(); - }); + $this->assertCount(2, $queryLog); - $router->get('/query/edit-columns', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->editColumn('id', function () { - return 'edited'; - }) - ->editOnlySelectedColumns() - ->editColumn('name', function () { - return 'edited'; - }) - ->editColumn('email', function () { - return 'edited'; - }) - ->toJson(); - }); + $sql = end($queryLog)['query']; + $this->assertStringContainsString("order by $expected asc", $sql); + } - $router->get('/query/xss-add', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->addColumn('foo', 'Allowed') - ->addColumn('bar', function () { - return 'Allowed'; - }) - ->toJson(); - }); + protected function setUp(): void + { + parent::setUp(); - $router->get('/query/xss-edit', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->editColumn('name', 'Allowed') - ->editColumn('email', function () { - return 'Allowed'; - }) - ->toJson(); - }); + $router = $this->app['router']; - $router->get('/query/xss-raw', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->addColumn('foo', 'Allowed') - ->editColumn('name', 'Allowed') - ->editColumn('email', function () { - return 'Allowed'; - }) - ->rawColumns(['name', 'email']) - ->toJson(); - }); + $router->get('/query/users', fn (DataTables $dataTable) => $dataTable->query(DB::table('users'))->toJson()); + + $router->get('/query/formatColumn', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->formatColumn('created_at', new DateFormatter('Y-m-d')) + ->toJson()); + + $router->get('/query/addColumn', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->addColumn('foo', 'bar') + ->toJson()); + + $router->get('/query/indexColumn', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->addIndexColumn() + ->toJson()); + + $router->get('/query/filterColumn', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->addColumn('foo', 'bar') + ->filterColumn('foo', function (Builder $builder, $keyword) { + $builder->where('1', $keyword); + }) + ->toJson()); + + $router->get('/query/blacklisted-filter', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->addColumn('foo', 'bar') + ->filterColumn('foo', function (Builder $builder, $keyword) { + $builder->where('name', $keyword); + }) + ->blacklist(['foo']) + ->toJson()); + + $router->get('/query/only', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->addColumn('foo', 'bar') + ->only(['name']) + ->toJson()); + + $router->get('/query/edit-columns', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->editColumn('id', fn () => 'edited') + ->editOnlySelectedColumns() + ->editColumn('name', fn () => 'edited') + ->editColumn('email', fn () => 'edited') + ->toJson()); + + $router->get('/query/xss-add', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->addColumn('foo', 'Allowed') + ->addColumn('bar', fn () => 'Allowed') + ->toJson()); + + $router->get('/query/xss-edit', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->editColumn('name', 'Allowed') + ->editColumn('email', fn () => 'Allowed') + ->toJson()); + + $router->get('/query/xss-raw', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->addColumn('foo', 'Allowed') + ->editColumn('name', 'Allowed') + ->editColumn('email', fn () => 'Allowed') + ->rawColumns(['name', 'email']) + ->toJson()); $router->get('/query/search-panes', function (DataTables $dataTable) { $options = User::select('id as value', 'name as label')->get(); return $dataTable->query(DB::table('users')) - ->searchPane('id', $options) - ->toJson(); - }); - - $router->get('/set-total-records', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->setTotalRecords(10) - ->toJson(); + ->searchPane('id', $options) + ->toJson(); }); - $router->get('/zero-total-records', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->setTotalRecords(0) - ->toJson(); - }); - - $router->get('/set-filtered-records', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->setFilteredRecords(10) - ->toJson(); - }); - - $router->get('/closure-di', function (DataTables $dataTable) { - return $dataTable->query(DB::table('users')) - ->addColumn('name_di', function ($user, User $u) { - return $u->newQuery()->find($user->id)->name.'_di'; - }) - ->toJson(); - }); + $router->get('/set-total-records', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->setTotalRecords(10) + ->toJson()); + + $router->get('/zero-total-records', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->setTotalRecords(0) + ->toJson()); + + $router->get('/skip-total-records', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->skipTotalRecords() + ->toJson()); + + $router->get('/set-filtered-records', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->setFilteredRecords(10) + ->toJson()); + + $router->get('/closure-di', fn (DataTables $dataTable) => $dataTable->query(DB::table('users')) + ->addColumn('name_di', fn ($user, User $u) => $u->newQuery()->find($user->id)->name.'_di') + ->toJson()); + + $router->get('/query/aliases', fn (DataTables $dataTable) => $dataTable->query( + DB::table('posts') + ->join('users', 'users.id', '=', 'posts.user_id') + ->select([ + 'posts.*', + 'users.email', // From join table, without alias + 'posts.id as alias_field_with_as', + DB::raw('posts.id alias_field_without_as'), + DB::raw('(1 + 1) as alias_expression_with_as'), + DB::raw('(SELECT 1) alias_expression_without_as'), + DB::raw('CASE WHEN 1 THEN 1 ELSE 2 END as alias_case_with_as'), + DB::raw('CASE WHEN 0 THEN 1 ELSE 2 END alias_case_without_as'), + 'sub_query' => DB::table('users') + ->whereColumn('posts.user_id', 'users.id') + ->select('name'), + ]) + )->toJson()); } } diff --git a/tests/Unit/HelperTest.php b/tests/Unit/HelperTest.php index ef01e0b0..65ac587c 100644 --- a/tests/Unit/HelperTest.php +++ b/tests/Unit/HelperTest.php @@ -72,7 +72,7 @@ public function test_compile_content_blade() { $content = '{!! $id !!}'; $data = ['id' => 2]; - $obj = new stdClass(); + $obj = new stdClass; $obj->id = 2; $compiled = Helper::compileContent($content, $data, $obj); @@ -83,7 +83,7 @@ public function test_compile_content_string() { $content = 'string'; $data = ['id' => 2]; - $obj = new stdClass(); + $obj = new stdClass; $obj->id = 2; $compiled = Helper::compileContent($content, $data, $obj); @@ -94,7 +94,7 @@ public function test_compile_content_integer() { $content = 1; $data = ['id' => 2]; - $obj = new stdClass(); + $obj = new stdClass; $obj->id = 2; $compiled = Helper::compileContent($content, $data, $obj); @@ -103,11 +103,9 @@ public function test_compile_content_integer() public function test_compile_content_function() { - $content = function ($obj) { - return $obj->id; - }; + $content = fn ($obj) => $obj->id; $data = ['id' => 2]; - $obj = new stdClass(); + $obj = new stdClass; $obj->id = 2; $compiled = Helper::compileContent($content, $data, $obj); @@ -124,7 +122,7 @@ public function __invoke($obj) } }; $data = ['id' => 2]; - $obj = new stdClass(); + $obj = new stdClass; $obj->id = 2; $compiled = Helper::compileContent($content, $data, $obj); @@ -147,7 +145,7 @@ public function test_get_mixed_value() 'name' => 'John', 'created_at' => '1234', ]; - $class = new stdClass(); + $class = new stdClass; $class->id = 1; $class->name = 'John'; $class->created_at = $carbon; @@ -164,7 +162,7 @@ public function test_get_mixed_value() public function test_cast_to_array_an_object() { - $class = new stdClass(); + $class = new stdClass; $class->id = 1; $compiled = Helper::castToArray($class); $this->assertEquals(['id' => 1], $compiled); @@ -190,11 +188,11 @@ public function test_get_or_method() public function test_convert_to_array() { - $row = new stdClass(); + $row = new stdClass; $row->id = 1; $row->name = 'John'; $row->posts = ['id' => 1, 'title' => 'Demo']; - $author = new stdClass(); + $author = new stdClass; $author->name = 'Billy'; $row->author = $author; diff --git a/tests/Unit/QueryDataTableTest.php b/tests/Unit/QueryDataTableTest.php index 177b6796..20864a72 100644 --- a/tests/Unit/QueryDataTableTest.php +++ b/tests/Unit/QueryDataTableTest.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\Tests\Models\User; use Yajra\DataTables\Tests\TestCase; @@ -40,9 +41,9 @@ public function test_complex_query_use_select_in_count() ->select('users.*') ->addSelect([ 'last_post_id' => DB::table('posts') - ->whereColumn('posts.user_id', 'users.id') - ->orderBy('created_at') - ->select('id'), + ->whereColumn('posts.user_id', 'users.id') + ->orderBy('created_at') + ->select('id'), ]) ->orderBy( DB::table('posts')->whereColumn('posts.user_id', 'users.id')->orderBy('created_at')->select('created_at') @@ -61,9 +62,9 @@ public function test_complex_query_can_ignore_select_in_count() ->select('users.*') ->addSelect([ 'last_post_id' => DB::table('posts') - ->whereColumn('posts.user_id', 'users.id') - ->orderBy('created_at') - ->select('id'), + ->whereColumn('posts.user_id', 'users.id') + ->orderBy('created_at') + ->select('id'), ]) ->orderBy( DB::table('posts')->whereColumn('posts.user_id', 'users.id')->orderBy('created_at')->select('created_at') @@ -74,25 +75,60 @@ public function test_complex_query_can_ignore_select_in_count() $this->assertEquals(20, $dataTable->count()); } - public function test_simple_queries_with_complexe_select_are_wrapped_without_selects() + public function test_simple_queries_with_complexe_select_are_not_wrapped() { /** @var \Yajra\DataTables\QueryDataTable $dataTable */ $dataTable = app('datatables')->of( DB::table('users') - ->select('users.*') - ->addSelect([ - 'last_post_id' => DB::table('posts') - ->whereColumn('posts.user_id', 'users.id') - ->orderBy('created_at') - ->select('id'), - ]) + ->select('users.*') + ->addSelect([ + 'last_post_id' => DB::table('posts') + ->whereColumn('posts.user_id', 'users.id') + ->orderBy('created_at') + ->select('id'), + ]) ); - $this->assertQueryWrapped(true, $dataTable->prepareCountQuery()); - $this->assertQueryHasNoSelect(true, $dataTable->prepareCountQuery()); + $this->assertQueryWrapped(false, $dataTable->prepareCountQuery()); $this->assertEquals(20, $dataTable->count()); } + public function test_simple_queries_with_complexe_where_are_not_wrapped() + { + /** @var \Yajra\DataTables\QueryDataTable $dataTable */ + $dataTable = app('datatables')->of( + DB::table('users') + ->select('users.*') + ->where( + DB::table('posts') + ->whereColumn('posts.user_id', 'users.id') + ->orderBy('created_at') + ->select('title'), 'User-1 Post-1' + ) + ); + + $this->assertQueryWrapped(false, $dataTable->prepareCountQuery()); + $this->assertEquals(1, $dataTable->prepareCountQuery()->count()); + } + + public function test_simple_eloquent_queries_with_complexe_where_are_not_wrapped() + { + /** @var \Yajra\DataTables\QueryDataTable $dataTable */ + $dataTable = app('datatables')->of( + User::query() + ->select('users.*') + ->where( + DB::table('posts') + ->whereColumn('posts.user_id', 'users.id') + ->orderBy('created_at') + ->select('title'), 'User-1 Post-1' + ) + ); + + $this->assertQueryWrapped(false, $dataTable->prepareCountQuery()); + $this->assertEquals(1, $dataTable->prepareCountQuery()->count()); + } + public function test_simple_queries_are_not_wrapped_and_countable() { /** @var \Yajra\DataTables\QueryDataTable $dataTable */ @@ -117,10 +153,9 @@ public function test_complexe_queries_can_be_wrapped_and_countable() /** * @param $expected bool - * @param $query \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder - * @return void + * @param $query \Illuminate\Contracts\Database\Query\Builder */ - protected function assertQueryWrapped($expected, $query) + protected function assertQueryWrapped($expected, $query): void { $sql = $query->toSql(); @@ -129,13 +164,50 @@ protected function assertQueryWrapped($expected, $query) /** * @param $expected bool - * @param $query \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder - * @return void + * @param $query \Illuminate\Contracts\Database\Query\Builder */ - public function assertQueryHasNoSelect($expected, $query) + public function assertQueryHasNoSelect($expected, $query): void { - $sql = $query->toSql(); + $sql = $query->select(DB::raw('count(*)'))->toSql(); + + $this->assertSame($expected, Str::startsWith($sql, 'select count(*) from (select 1 as dt_row_count from'), "'{$sql}' has select"); + } + + #[Test] + public function test_column_name_is_resolved_in_column_control(): void + { + app('datatables.request')->merge([ + 'columns' => [ + [ + 'name' => 'id', + 'data' => 'id', + 'searchable' => 'true', + 'orderable' => 'true', + 'search' => ['value' => null, 'regex' => 'false'], + 'columnControl' => [ + 'search' => [ + 'value' => '123', + 'logic' => 'equal', + 'type' => 'num', + ], + ], + ], + ], + ]); + + /** @var \Yajra\DataTables\QueryDataTable $dataTable */ + $dataTable = app('datatables')->of( + User::query() + ->select('users.*') + ->join('role_user', 'users.id', '=', 'role_user.user_id') + ->join('roles', 'role_user.role_id', '=', 'roles.id') + ); - $this->assertSame($expected, Str::startsWith($sql, 'select * from (select 1 as dt_row_count from'), "'{$sql}' is not wrapped"); + $dataTable->columnControlSearch(); + + $this->assertStringContainsString( + '"users"."id" = \'123\'', + $dataTable->getQuery()->toRawSql() + ); } } diff --git a/tests/Unit/RequestTest.php b/tests/Unit/RequestTest.php index 76039f75..cbcdfaa8 100644 --- a/tests/Unit/RequestTest.php +++ b/tests/Unit/RequestTest.php @@ -2,12 +2,13 @@ namespace Yajra\DataTables\Tests\Unit; +use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\Tests\TestCase; use Yajra\DataTables\Utilities\Request; class RequestTest extends TestCase { - /** @test */ + #[Test] public function it_can_get_the_base_request() { $request = $this->getRequest(); @@ -15,7 +16,15 @@ public function it_can_get_the_base_request() $this->assertInstanceOf(\Illuminate\Http\Request::class, $request->getBaseRequest()); } - /** @test */ + /** + * @return \Yajra\DataTables\Utilities\Request + */ + protected function getRequest() + { + return new Request; + } + + #[Test] public function it_is_searchable() { $_GET['search']['value'] = ''; @@ -34,7 +43,7 @@ public function it_is_searchable() $this->assertTrue($request->isSearchable()); } - /** @test */ + #[Test] public function it_can_get_column_keyword() { $_GET['columns'] = []; @@ -55,7 +64,7 @@ public function it_can_get_column_keyword() $this->assertEquals('bar', $request->columnKeyword(1)); } - /** @test */ + #[Test] public function it_has_orderable_columns() { $_GET['columns'] = []; @@ -80,7 +89,7 @@ public function it_has_orderable_columns() $this->assertTrue($request->isColumnOrderable(0)); } - /** @test */ + #[Test] public function it_has_will_set_descending_on_other_values_on_orderable_columns() { $_GET['columns'] = []; @@ -105,7 +114,7 @@ public function it_has_will_set_descending_on_other_values_on_orderable_columns( $this->assertTrue($request->isColumnOrderable(0)); } - /** @test */ + #[Test] public function it_has_searchable_column_index() { $_GET['columns'] = []; @@ -125,7 +134,7 @@ public function it_has_searchable_column_index() $this->assertEquals('bar', $request->columnName(1)); } - /** @test */ + #[Test] public function it_has_keyword() { $_GET['search'] = []; @@ -135,7 +144,7 @@ public function it_has_keyword() $this->assertEquals('foo', $request->keyword()); } - /** @test */ + #[Test] public function it_is_paginationable() { $_GET['start'] = 1; @@ -156,12 +165,4 @@ public function it_is_paginationable() $request = $this->getRequest(); $this->assertFalse($request->isPaginationable()); } - - /** - * @return \Yajra\DataTables\Utilities\Request - */ - protected function getRequest() - { - return new Request(); - } }