diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..8791d9f2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1185e157 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto + +/tests export-ignore +.github/ export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.php_cs export-ignore +.scrutinizer.yml export-ignore +.travis.yml export-ignore +phpunit.xml export-ignore diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 95% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index 5589f354..89e4aa5c 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,7 +2,7 @@ Contributions are **welcome** and will be fully **credited**. -We accept contributions via Pull Requests on [Github](https://github.com/yajra/laravel-datatables-oracle). +We accept contributions via Pull Requests on [Github](https://github.com/yajra/laravel-datatables). ## Pull Requests diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..7e0746cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,31 @@ + +### Summary of problem or feature request + + + + +### Code snippet of problem + + + +### System details + + + +- Operating System +- PHP Version +- Laravel Version +- Laravel-Datatables Version diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..7f8e07d9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ + diff --git a/.gitignore b/.gitignore index ae89308a..df0e1ea8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /vendor +/coverage composer.phar composer.lock -/coverage diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 9a20c997..85add7e0 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -7,3 +7,4 @@ filter: excluded_paths: - 'tests/*' - 'vendor/*' + - '.github/*' diff --git a/.travis.yml b/.travis.yml index 3925bae8..99505ffc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,10 @@ php: - 7.0 - hhvm +matrix: + allow_failures: + - php: hhvm + env: global: - setup=basic diff --git a/CHANGELOG.md b/CHANGELOG.md index e1fb4b6d..2bcff071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,269 +8,617 @@ ##Change Log +### v6.29.3 - 2018-04-05 +- Fixed a bug for "undefined index" errors. #1675, credits to @redelschaap. + +### v6.29.2 - 2017-12-08 +- Fix issue #932. #1543, credits to @abiodunjames. + +### v6.29.1 - 2017-09-26 +- Return true for true, true(string) and 1 in isColumnSearchable method #1395, credits to @joeshee. + +### v6.29.0 - 2017-06-07 +- Add config for setting the default JsonResponse header and options. #1179 +- Fix #1034 +- TODO: Need to update tests. + +### v6.28.1 - 2017-05-05 +- Fixed: Formatting the attributes for Editor buttons. #1105, credits to @karmendra. + +### v6.28.0 - 2017-04-19 +- Display url and A balise as hyperlink in excel export. +- PR #1074, credits to @julianfox. + +### v6.27.0 - 2017-03-08 +- Add a fluent way to send route variables to DataTable service class. PR #1071. +- Fix #1069. + +### v6.26.0 - 2017-03-03 +- Remove dompdf dependency and let the developers decide on which version they want to use. +> Note: This is possible breaking change for some users but needed to move forward. See https://github.com/yajra/laravel-datatables/pull/1026 for details. Thanks! + +### v6.25.0 - 2017-02-23 +- Add support ordering when search in nested relations. #850 +- Credits to @ethaizone. + +### v6.24.4 - 2017-02-09 +- Fix collection case insensitive ordering. +- Fix [#945](https://github.com/yajra/laravel-datatables/issues/945). + +### v6.24.3 - 2017-02-08 +- Add bindings from relations. #981 +- Implementation from PR #962 to fix #960. + +### v6.24.2 - 2017-01-26 +- Lock v6.x to support Laravel 5.0 - 5.3 only. +- Fix #942. + +### v6.24.1 - 2017-01-24 +- Fix regex query for pgsql. +- PR #934, credits to @alfa6661. + +### v6.24.0 - 2017-01-24 +- Datatables Pipeline Plugin support (Basic). +- PR #938, credits to @OzanKurt. + +### v6.23.0 - 2017-01-21 +- Add support for onlyTrashed model scope. Fix #928 +- Move withTrashed method to Eloquent engine. + +### v6.22.9 - 2017-01-20 +- Do not append table name if instance is expression. #933 +- Fix #927 and possible fix for #873. + +### v6.22.8 - 2017-01-19 +- Support for UTF8 characters when creating wildcard query. +- PR #926, credits to @Rhincodon. + +### v6.22.7 - 2017-01-04 +- Add datatables alias to fix singleton. PR #903, credits to @ElfSundae +- Use static class name instead FQCN. PR #902, credits to @ElfSundae +- Update license to 2017. + +### v6.22.6 - 2017-01-04 +- Fix print header issue when two row are same value. +- PR #913, credits to @dineshrabara. + +### v6.22.5 - 2016-12-07 +- Fix double wrapping of column name. + +### v6.22.4 - Skipped (Sorry!) + +### v6.22.3 - 2016-12-07 +- Fix adding of table prefix before casting the column. PR #876 +- Fix #871, #873 +- Custom soft delete column support. PR #878, credits to @Huracan88 +- Fix #875 + +### v6.22.2 - 2016-12-01 +- Patch for ambiguous field error for relations issues. +- PR #849, credits to @ethaizone. + +### v6.22.1 - 2016-12-01 +- Patch request class and use input array access to get the values. #868 +- Fix Error 500 Illegal string offset issues: #857, #657, #395. + +### v6.22.0 - 2016-11-19 +- Add support for searching of nested relationships. +- PR #841 & #844. Credits to @ethaizone +- Fix #696, #789, #771, #509, #441. + +### v6.21.2 - 2016-11-16 +- Fix html builder instance, close #830. PR #833, credits to @ElfSundae. +- Class name resolution updated. PR #812, credits to @shibby. + +### v6.21.1 - 2016-10-25 +- Do not convert column to raw statement. Thanks to Nick Howell for pointing the issue. + +### v6.21.0 - 2016-10-22 +- Support to morphToMany. PR #804, credits to @pcastrotigre +- Update fractal to ~0.14. Credits to @tyloo +- Refactor HasOneOrMany as suggested in #733 +- Fix CS. + +### v6.20.0 - 2016-10-13 +- Support of using soft deletes trait in models. +- PR #786, credits to @DDiimmkkaass. + +### v6.19.0 - 2016-09-24 +- Support regex column searches when using CollectionEngine. +- PR #765, credits to @cadenzajon. + +### v6.18.2 - 2016-09-13 +- Make snappy pdf generation configurable via datatables config file. +- Fix loading of printPreview using loadHTML. + +### v6.18.1 - 2016-09-05 +- Fix to allow join of eager loaded column based on a HasMany-relationship. +- Fix #732, PR #733 credits to @SRautila. + +### v6.18.0 - 2016-08-25 +- Fix eager loaded model searching function. Fix #698 and #710. +- Add method to skip pagination. Credits to @Nks PR #707 +- Fix collection filtered total records. If filtered total > total records, use total records. + +### v6.17.1 - 2016-08-23 +- Use dompdf v0.7.x. +- Implement/fix setTotalRecords on collection engine. + +### v6.17.0 - 2016-08-10 +- Add setter and getter for table attributes. +- PR #688, credits to @ssipos90. + +### v6.16.1 - 2016-08-03 +- Add orderColumns api helper. PR #679 +- Fix double percent sign in query. PR #678 + +### v6.16.0 - 2016-07-19 +- Allow global search to work with custom filter callback. #644 +- Transformer instance is kept, otherwise new instance is created. #649 +- Remove unused import and fix cs. + +### v6.15.2 - 2016-07-14 +- Fix the issue where a record is being deleted by LDT when column name used is delete. +- Fix helper doc blocks. +- Fix #565 and #640. +- Allow travis-ci failure on HHVM. Mostly due to execution timestamp. + +### v6.15.1 - 2016-07-13 +- Decouple fractal for better integration. #636 +- Fractal manager instance can be accessed via app('datatables.fractal'). + +### v6.15.0 - 2016-07-05 +- Add support for snappy pdf via config. +- Add laravel-snappy on suggested packages. +- Add table stripes style. +- Fix printing template when value is an array. +- Fix issue when exporting pdf and data can't be decoded. +- ErrorException in DataTransformer.php line 67: strip_tags() expects parameter 1 to be string, array given + +### v6.14.0 - 2016-07-01 +- Add model option to dataTable command. #620 +- Make generator namespace configurable. +- Add filename in generator with timestamps. + +### v6.13.1 - 2016-06-23 +- Add option to manually set the total records. + -New method: ->setTotalRecords(int total) +- Addresses count queries performance issue like #578. + +### v6.13.0 - 2016-06-23 +- Add Auto-Index Column. #597 +- Add method 'addIndexColumn()' to compliment PR#597. + - Note: addIndexColumn can be used without the builder. +- Add config file descriptions for reference. + +### v6.12.0 - 2016-06-23 +- Add support for ajax data function. #613 +- Update dataTable service query return docblock and stub. + +### v6.11.5 - 2016-06-01 +- Fix HasOne relation which uses different methods to get foreign and other key. #585 + +###v6.11.4 - 2016-05-30 +- Remove media screen to fix styles when printing. #583 + +###v6.11.3 - 2016-05-20 +- Add export button collection. #568 +- Fix default print preview view path. #569 + +###v6.11.2 - 2016-05-18 +- Add CAST for Firebird #552. + +###v6.11.1 - 2016-05-11 +- Use Str class helper instead of strlen for better unicode support. +- Change method from private to protected as requested on #544. + +###v6.11.0 - 2016-04-30 +- Patch phpdoc to fix #531. PR #534 +- Fix eager loading ordering for belongsToMany relationship. Fix #461, PR #490 +- Add support for responsive extension. Fix #526, PR #533 +- Add option to create a table footer from builder defined via column def. PR #471 + +###v6.10.1 - 2016-03-22 +- Fix eager loading column search. PR #469. +- Fix issue #443. + +###v6.10.0 - 2016-03-19 +- Add feature to enable/disable smart search via config or during runtime. Fix #423 +- See PR #452 for details. + +###v6.9.4 - 2016-03-18 +- Use full namespace in app() helper. +- PR #465, credits to @ligne13. + +###v6.9.3 - 2016-03-17 +- Adds an option to pass parameters to column render. +- Allows passing instance of Column into Builder columns. +- Fix security issue as reported in #460. +- Credits to @vladkucherov for this changes. + +###v6.9.2 - 2016-03-16 +- Pull-up isOracleSQL and fix condition using oci8. + +###v6.9.1 - 2016-03-11 +- Add default array value when getting columns. Fix #448 + +###v6.9.0 - 2016-03-11 +- Re-implement facade. +- Add blacklist and whitelist feature. +- Fix string casting for object values. +- Add missing doc block for getSearchKeyword. +- Fix eloquent engine missing parent constructor. +- Add/Update class doc blocks. + +###v6.8.0 - 2016-03-11 +- Added Closure support for filterColumn method. +- PR #440. Credits to @codewizz. + +###v6.7.3 - 2016-03-02 +- Fix eager load multiple column sorting where other columns are being ignored when join statement already exists. +- Refactor redundant else order by statement. +- Call eager loads only when required when filtering and ordering. +- Extract eager loaded column join statement handler. + +###v6.7.2 - 2016-02-28 +- Fix collection engine sorting and sorting function. +- Fix #413 and #415. + +###v6.7.1 - 2016-02-26 +- Fix multiple column sorting when using eager loaded models. Fix #410 + +###v6.7.0 - 2016-02-26 +- Add support for sorting on eager loaded models. +- PR #409 - Credits to @ikerasLT. + +###v6.6.1 - 2016-02-20 +- Fix eager loading search (SQLSTATE[21000]: Cardinality violation:). Issue #403. + +###v6.6.0 - 2016-02-20 +- Add totalCount on contact and remove excess new line. +- Remove unnecessary abstract function on BaseEngine since we have a contract. +- Remove engine implementation of contract since BaseEngine already requires it. +- Improve column name detection for filtering and sorting. +- Dynamically determine if oracle depending on connection used. +- Automatic detection of primary key when using Eloquent engine. +- Use primary key when column name could not be resolve. +- Update DataTable service doc blocks and refactor render method. + +###v6.5.1 - 2016-02-19 +- Fix ordering column name detection. Issue #339. +- Refactor Builder parameterize method. + +###v6.5.0 - 2016-02-18 +- Add support for DataTables valid callbacks. +- Fix issue #387 & #401. + +###v6.4.5 - 2016-02-18 +- Allow edit columns for nested arrays. PR #399 - credits to @ramilexe +- Fix flag for case insensitive search. PR #400 - credits to @ansient + +###v6.4.4 - 2016-02-13 +- Fix filtering in nested columns of Collections. PR #392 + +###v6.4.3 - 2016-02-11 +- Random cs and doc block fix. +- Code refactoring to reduce complexity. + +###v6.4.2 - 2016-02-11 +- Change how regex code is generated after a column search. #358 +- Fix addColumn fails when order falls at end of array #386 + +###v6.4.1 - 2016-02-10 +- Fix nested eager loaded relations and column name. + +###v6.4.0 - 2016-02-10 +- Add feature to support global search on eager loaded models. +- PR #381. Credits to @ikerasLT. +- Fix implementation conflicts when using builder and join statements. +- Fix cs and doc blocks. + +###v6.3.2 - 2016-02-04 +- Add order by and group by on count sql optimization exceptions. +- Date will now be added on each released version using Y-m-d format. + +###v6.3.1 +- Fix artisan datatables:make service stub. + +###v6.3.0 +- Add option to override default ordering via `->order(\Closure $callback)` method. +- Add editor config. +- Add some new features docs. +- Remove Laravel 4.2 documentation on 6.0 branch. + +###v6.2.4 +- Add git attributes. + +###v6.2.3 +- Add setter/getter for filename. +- Add html_entity_decode when exporting file. +- Decode column title when exporting. + +###v6.2.2 +- Extract data transformation task to own class. +- Refactor duplicate response mapping code. +- Increase scrutinizer score. + +###v6.2.1 +- Fix data when exporting with html tags. +- Add filename method in stub. +- Fix some doc blocks. +- Scrutinizer refactoring. + +###v6.2.0 +- Enhance printing function to match what is displayed in UI. +- Enhance export function to match what is displayed in UI. +- Enhance datatables service stub. +- Address issue #310. +- Add option to set column as exportable and/or printable. +- Action and checkbox column is not exportable but printable by default. + ###v6.1.3 - - Fix logical bug with totalRecords and filteredRecords. Fix #333 +- Fix logical bug with totalRecords and filteredRecords. Fix #333 ###v6.1.2 - - Fix possible conflict with Laravel helpers.php file auto-loading. Fix #330. - - Update dataTable service class stub. +- Fix possible conflict with Laravel helpers.php file auto-loading. Fix #330. +- Update dataTable service class stub. ###v6.1.1 - - Fix ordering when using basic array response. Fix #322. +- Fix ordering when using basic array response. Fix #322. ###v6.1.0 - - Add support for Lumen. - - Fixes #317, #318. +- Add support for Lumen. +- Fixes #317, #318. ###v6.0.0 - DataTable Service implementation. - - Provides DataTable Service. - - Provides artisan command for creating a service. php artisan datatables:make UsersDataTable - - Provides artisan command for creating a DataTable scope. php artisan datatables:scope ActiveUserScope - - Provides built-in support for server-side buttons. (Formerly TableTools). - - Available buttons are csv, excel, pdf, print. - - Built-in support for exporting to CSV, EXCEL and PDF using Laravel-Excel. - - Built-in printer friendly view or create your own by overriding printPreview() method. - - Change of namespace from yajra\Datatables to Yajra\Datatables. - - Deprecated of() method when using DataTable service. - - Automatic registration of required 3rd party providers. - - [Laravel Excel](https://github.com/Maatwebsite/Laravel-Excel) - - [Laravel Collective HTML & Forms](https://github.com/LaravelCollective/html) - - Automatic registration of Datatables facade. - - HTML Builder with javascript from template. #298 - Credits to @vladkucherov. - - HTML Builder column render now accepts a string, view or closure. #300 - Credits to @vladkucherov - - Add resource on json response by using `->with('key', 'value')` method. #277 +- Provides DataTable Service. +- Provides artisan command for creating a service. php artisan datatables:make UsersDataTable +- Provides artisan command for creating a DataTable scope. php artisan datatables:scope ActiveUserScope +- Provides built-in support for server-side buttons. (Formerly TableTools). +- Available buttons are csv, excel, pdf, print. +- Built-in support for exporting to CSV, EXCEL and PDF using Laravel-Excel. +- Built-in printer friendly view or create your own by overriding printPreview() method. +- Change of namespace from yajra\Datatables to Yajra\Datatables. +- Deprecated of() method when using DataTable service. +- Automatic registration of required 3rd party providers. + - [Laravel Excel](https://github.com/Maatwebsite/Laravel-Excel) + - [Laravel Collective HTML & Forms](https://github.com/LaravelCollective/html) +- Automatic registration of Datatables facade. +- HTML Builder with javascript from template. #298 - Credits to @vladkucherov. +- HTML Builder column render now accepts a string, view or closure. #300 - Credits to @vladkucherov +- Add resource on json response by using `->with('key', 'value')` method. #277 ###v5.12.5 - - Get order column name from the request. Fix #307. +- Get order column name from the request. Fix #307. ###v5.12.4 - - Fix searching when aliasing a column. Fix #274. +- Fix searching when aliasing a column. Fix #274. ###v5.12.3 - - Remove checking of columns - name index and let setupColumnName method to identify the proper column name. +- Remove checking of columns - name index and let setupColumnName method to identify the proper column name. ###v5.12.2 - - Fix double prefix when using join queries. Fix #272, #273 +- Fix double prefix when using join queries. Fix #272, #273 ###v5.12.1 - - Fix support for PHP5.4. +- Fix support for PHP5.4. ###v5.12.0 - - Added support for Fractal Serializer. - - Added config for default serializer. - - Note: Should be used along with setTransformer method. - - Usage: +- Added support for Fractal Serializer. +- Added config for default serializer. +- Note: Should be used along with setTransformer method. +- Usage: return Datatables::of($model) - ->setTransformer(ModelTransformer::class) - ->setSerializer(ModelSerializer::class) - ->make(true); + ->setTransformer(ModelTransformer::class) + ->setSerializer(ModelSerializer::class) + ->make(true); ###v5.11.14 - - Sort by a multi-line 'select as' query. PR #245 +- Sort by a multi-line 'select as' query. PR #245 ###v5.11.13 - - Allow fractal v0.12 and up. Fix #237. +- Allow fractal v0.12 and up. Fix #237. ###v5.11.12 - - Use connection grammar to wrap columns and table name. +- Use connection grammar to wrap columns and table name. ###v5.11.11 - - Parse includes in fractal. Fix #225. +- Parse includes in fractal. Fix #225. ###v5.11.10 - - CollectionEngine: fix sorting of relation columns by using seralize (like filtering). PR #197. +- CollectionEngine: fix sorting of relation columns by using seralize (like filtering). PR #197. ###v5.11.9 - - Add fix for QueryBuilder: ORDER BY *. PR #194. +- Add fix for QueryBuilder: ORDER BY *. PR #194. ###v5.11.8 - - Skip search, order and pagination on empty records. - - Fix #149 and #176 empty collection error when using make(false). - - Fix credits to @gabrielwelsche. +- Skip search, order and pagination on empty records. +- Fix #149 and #176 empty collection error when using make(false). +- Fix credits to @gabrielwelsche. ###v5.11.7 - - Fix escaping of row when using eager loading. Fix #164 - - Add support for escaping rows using array dot notation when declaring escapeColumns. +- Fix escaping of row when using eager loading. Fix #164 +- Add support for escaping rows using array dot notation when declaring escapeColumns. Example: `->escapeColumns(['name', 'post.title'])` ###v5.11.6 - - Refactor eloquent and query builder engine duplicate codes. +- Refactor eloquent and query builder engine duplicate codes. ###v5.11.5 - - Fix issues on database prefix. #161 and #162 - - Fix database prefix value when using Eloquent. +- Fix issues on database prefix. #161 and #162 +- Fix database prefix value when using Eloquent. ###v5.11.4 - - Fix Undefined offset issue when using addColumn. - - Credits to @openvast for PR #158 +- Fix Undefined offset issue when using addColumn. +- Credits to @openvast for PR #158 ###v5.11.3 - - Pass object or array to transformer. PR #155 +- Pass object or array to transformer. PR #155 ###v5.11.2 - - Add support for regular expressions search on column. +- Add support for regular expressions search on column. ###v5.11.1 - - Collection engine enhancement. - - Add support for filtering compound key PR #146. - - Add support for ordering using compound key. +- Collection engine enhancement. +- Add support for filtering compound key PR #146. +- Add support for ordering using compound key. ###v5.11.0 - - Add support for rendering view directly on addColumn and editColumn. +- Add support for rendering view directly on addColumn and editColumn. ###v5.10.0 - - Add LaravelDataTables on js window namespace. Issue #129. Credits to @greabock. +- Add LaravelDataTables on js window namespace. Issue #129. Credits to @greabock. ###v5.9.2 - - Fix possible error when rendering table and overriding the attributes. - - Merge DT parameters. +- Fix possible error when rendering table and overriding the attributes. +- Merge DT parameters. ###v5.9.1 - - Fix default ajax value causing js data null error. +- Fix default ajax value causing js data null error. ###v5.9.0 - - Added escapeColumns feature to escape the values. - - Addresses XSS filtering issue #128. +- Added escapeColumns feature to escape the values. +- Addresses XSS filtering issue #128. ###v5.8.6 - - Fix DT_Row options when returning a flatten array response. - - Fix PR #126. +- Fix DT_Row options when returning a flatten array response. +- Fix PR #126. ###v5.8.5 - - Revert try-catch when compiling blade. - - Fix html builder unit test. +- Revert try-catch when compiling blade. +- Fix html builder unit test. ###v5.8.4 - - Fix html builder merging of column attributes. +- Fix html builder merging of column attributes. ###v5.8.3 - - Added space when setting html builder table attributes. - - Set a default data value when adding a column. - - Removed unnecessary slash when getting html builder. - - Added html builder unit test. - - Improved test coverage. +- Added space when setting html builder table attributes. +- Set a default data value when adding a column. +- Removed unnecessary slash when getting html builder. +- Added html builder unit test. +- Improved test coverage. ###v5.8.2 - - Fix count when using DISTINCT query. Fix #125 +- Fix count when using DISTINCT query. Fix #125 ###v5.8.1 - - Fix compatiblity with PHP 5.4. +- Fix compatiblity with PHP 5.4. ###v5.8.0 - - Enhanced html builder class. - - Added function to load html builder `columns` via mixed array. - - Automatic resolution of qualified title based on field name. - - Overriding of column attributes. - - Added html builder and request object getter from main Datatables class. - - Added more unit tests. +- Enhanced html builder class. +- Added function to load html builder `columns` via mixed array. + - Automatic resolution of qualified title based on field name. + - Overriding of column attributes. +- Added html builder and request object getter from main Datatables class. +- Added more unit tests. ###v5.7.0 - - Added orderColumn feature. +- Added orderColumn feature. ###v5.6.1 - - Make BaseEngine $request property public. - - Fix global searching when search value is zero (0). - - Refactor methods from v5.6.0. +- Make BaseEngine $request property public. +- Fix global searching when search value is zero (0). +- Refactor methods from v5.6.0. ###v5.6.0 - - Re-implement filterColumn function with special variable $1. - - Fix filterColumn not getting included on OR statements within global search. - - Fix #115. +- Re-implement filterColumn function with special variable $1. +- Fix filterColumn not getting included on OR statements within global search. +- Fix #115. ###v5.5.11 - - Fix ordering for when using column alias and make(false). Fix #103. +- Fix ordering for when using column alias and make(false). Fix #103. ###v5.5.10 - - Fix casting specific to stdClass only. Fix #114. +- Fix casting specific to stdClass only. Fix #114. ###v5.5.9 - - Fix ordering of collection when data is stdClass. +- Fix ordering of collection when data is stdClass. ###v5.5.8 - - Fix issue when converting object to array. Fix #108. +- Fix issue when converting object to array. Fix #108. ###v5.5.7 - - Fix and enhance support when passing object variables using blade templating approach. - - Random code clean-up. +- Fix and enhance support when passing object variables using blade templating approach. +- Random code clean-up. ###v5.5.6 - - Fix eager loading of hasOne and hasMany relationships. Issue #105. +- Fix eager loading of hasOne and hasMany relationships. Issue #105. ###v5.5.5 - - Fix collection engine sorting when columns is not defined +- Fix collection engine sorting when columns is not defined ###v5.5.4 - - Fix support for collection of objects +- Fix support for collection of objects ###v5.5.3 - - Fix total filtered records count when overriding global search. - - Fix implementation of PR #95 on Collection Engine. +- Fix total filtered records count when overriding global search. +- Fix implementation of PR #95 on Collection Engine. ###v5.5.2 - - Fix database driver detection on Eloquent Engine. +- Fix database driver detection on Eloquent Engine. ###v5.5.1 - - Fix missing import of Helper class. +- Fix missing import of Helper class. ###v5.5.0 - - Refactor classes to improve code quality. - - Implemented PR #95. +- Refactor classes to improve code quality. +- Implemented PR #95. ###v5.4.4 - - Added column wrapper for SQLITE. +- Added column wrapper for SQLITE. ###v5.4.3 - - Added column wrapper for Postgres. Bugfix #82. +- Added column wrapper for Postgres. Bugfix #82. ###v5.4.2 - - Throws Exception when using DataTable's legacy code. - - Fixed CS - PSR2. +- Throws Exception when using DataTable's legacy code. +- Fixed CS - PSR2. ###v5.4.1 - - Fixed Builder generateScript method. +- Fixed Builder generateScript method. ###v5.4 - - Added Html Builder. - - Added magic methods to call enginges without the "using" word. - - Minor Bugfixes. +- Added Html Builder. +- Added magic methods to call enginges without the "using" word. +- Minor Bugfixes. ###v5.3 - - Added scrutinizer. - - Code refactor/cleanup based on scrutinizers suggestions. - - Bugfix #75. +- Added scrutinizer. +- Code refactor/cleanup based on scrutinizers suggestions. +- Bugfix #75. ###v5.2 - - Datatables can now be used via Laravel IOC container `app('datatables')`. - - Datatables Engine can now be used directly along with Laravel IOC. - - Available Engines: - - Query Builder Engine. `app('datatables')->usingQueryBuilder($builder)->make()`. - - Eloquent Engine. `app('datatables')->usingEloquent($model)->make()`. - - Collection Engine. `app('datatables')->usingCollection($collection)->make()`. - - Datatables is now more testable and works with https://github.com/laracasts/integrated. - - Bugfix #56. +- Datatables can now be used via Laravel IOC container `app('datatables')`. +- Datatables Engine can now be used directly along with Laravel IOC. + - Available Engines: + - Query Builder Engine. `app('datatables')->usingQueryBuilder($builder)->make()`. + - Eloquent Engine. `app('datatables')->usingEloquent($model)->make()`. + - Collection Engine. `app('datatables')->usingCollection($collection)->make()`. +- Datatables is now more testable and works with https://github.com/laracasts/integrated. +- Bugfix #56. ###v5.1 - - Added filterColumn function to override default global search in each column. - - Datatables class extending Query Builder's functionality along with global search. - - Restore queries on result when app is in debug mode. - - Added input on result when app is in debug mode. - - Force enable query log when app is in debug mode. - - Convert string search from preg_match to Str::contains to improve performance. - - Added support for having clause queries. - - Added support for `league/fractal` for transforming data API output. +- Added filterColumn function to override default global search in each column. +- Datatables class extending Query Builder's functionality along with global search. +- Restore queries on result when app is in debug mode. +- Added input on result when app is in debug mode. +- Force enable query log when app is in debug mode. +- Convert string search from preg_match to Str::contains to improve performance. +- Added support for having clause queries. +- Added support for `league/fractal` for transforming data API output. ###v5.0 - - Strictly for Laravel 5++. - - Drop support for DT1.9 and below. - - Strict implmentation of DT1.10 script pattern. - - Added support for Collection as data source. +- Strictly for Laravel 5++. +- Drop support for DT1.9 and below. +- Strict implmentation of DT1.10 script pattern. +- Added support for Collection as data source. ###v4.3.x - - Stable version for Laravel 5 with support for DT1.9. - - Collection Engine not available. +- Stable version for Laravel 5 with support for DT1.9. +- Collection Engine not available. ###v3.6.x - - Stable version for Laravel 4.2. +- Stable version for Laravel 4.2. ###v2.x - - Stable version for Laravel 4.0 and 4.1 +- Stable version for Laravel 4.0 and 4.1 diff --git a/CONDUCT.md b/CONDUCT.md new file mode 100644 index 00000000..6ff94ca3 --- /dev/null +++ b/CONDUCT.md @@ -0,0 +1,22 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic addresses, without explicit permission +* Other unethical or unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. + +This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community in a direct capacity. Personal views, beliefs and values of individuals do not necessarily reflect those of the organisation or affiliated individuals and organisations. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) diff --git a/LICENSE.md b/LICENSE.md index 7414309d..8cfbdf84 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ (The MIT License) -Copyright (c) 2013-2015 Arjay Angeles +Copyright (c) 2013-2017 Arjay Angeles Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md new file mode 100644 index 00000000..2e536d56 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# jQuery DataTables API for Laravel 4|5 + +[![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) + +[![Laravel 4.2|5.x](https://img.shields.io/badge/Laravel-4.2|5.x-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) +[![Build Status](https://travis-ci.org/yajra/laravel-datatables.svg?branch=master)](https://travis-ci.org/yajra/laravel-datatables) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/yajra/laravel-datatables/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/yajra/laravel-datatables/?branch=master) +[![Total Downloads](https://img.shields.io/packagist/dt/yajra/laravel-datatables-oracle.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) + +This package is created to handle [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 +use Yajra\Datatables\Facades\Datatables; + +// Using Eloquent +return Datatables::eloquent(User::query())->make(true); + +// Using Query Builder +return Datatables::queryBuilder(DB::table('users'))->make(true); + +// Using Collection +return Datatables::collection(User::all())->make(true); + +// Using the Engine Factory +return Datatables::of(User::query())->make(true); +return Datatables::of(DB::table('users'))->make(true); +return Datatables::of(User::all())->make(true); +``` + +## Requirements +- [PHP >=5.5.9](http://php.net/) +- [Laravel 5.x](https://github.com/laravel/framework) +- [jQuery DataTables v1.10.x](http://datatables.net/) + +## Documentations +- [Laravel Datatables Documentation](https://yajrabox.com/docs/laravel-datatables/6.0) +- [Laravel Datatables API](http://yajra.github.io/laravel-datatables/api/6.0) +- [Demo Application](http://datatables.yajrabox.com) is available for artisan's reference. + +## Quick Installation +`composer require yajra/laravel-datatables-oracle:~6.0` + +#### Service Provider +`Yajra\Datatables\DatatablesServiceProvider::class` + +#### Facade +`Datatables` facade is automatically registered as an alias for `Yajra\Datatables\Facades\Datatables` class. + +#### Configuration and Assets +`$ php artisan vendor:publish --tag=datatables` + +And that's it! Start building out some awesome DataTables! + +## Debugging Mode +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. + +## Contributing + +Please see [CONTRIBUTING](https://github.com/yajra/laravel-datatables/blob/master/.github/CONTRIBUTING.md) for details. + +## Security + +If you discover any security related issues, please email [aqangeles@gmail.com](mailto:aqangeles@gmail.com) instead of using the issue tracker. + +## Credits + +- [Arjay Angeles](https://github.com/yajra) +- [bllim/laravel4-datatables-package](https://github.com/bllim/laravel4-datatables-package) +- [All Contributors](https://github.com/yajra/laravel-datatables/graphs/contributors) + +## License + +The MIT License (MIT). Please see [License File](https://github.com/yajra/laravel-datatables/blob/master/LICENSE.md) for more information. + +## Buy me a coffee +- Click here to lend your support to: Laravel Datatables and make a donation at pledgie.com ! +- Become a Patron diff --git a/composer.json b/composer.json index e98e42f8..33c281b5 100644 --- a/composer.json +++ b/composer.json @@ -11,24 +11,31 @@ ], "require": { "php": ">=5.5.9", - "illuminate/support": "~5.0", - "illuminate/database": "~5.0", - "illuminate/view": "~5.0", - "illuminate/http": "~5.0", - "illuminate/filesystem": "~5.0", - "league/fractal": "~0.12", - "laravelcollective/html": "~5.0", - "maatwebsite/excel": "^2.0", - "dompdf/dompdf": "^0.6.1" + "illuminate/support": "5.0.*|5.1.*|5.2.*|5.3.*", + "illuminate/database": "5.0.*|5.1.*|5.2.*|5.3.*", + "illuminate/view": "5.0.*|5.1.*|5.2.*|5.3.*", + "illuminate/http": "5.0.*|5.1.*|5.2.*|5.3.*", + "illuminate/filesystem": "5.0.*|5.1.*|5.2.*|5.3.*", + "league/fractal": "~0.14", + "laravelcollective/html": "5.0.*|5.1.*|5.2.*|5.3.*", + "maatwebsite/excel": "^2.0" }, "require-dev": { "mockery/mockery": "~0.9", "phpunit/phpunit": "~4.0" }, + "suggest": { + "barryvdh/laravel-snappy": "Allows exporting of dataTable to PDF using the print view." + }, "autoload": { "psr-4": { "Yajra\\Datatables\\": "src/" } }, + "extra": { + "branch-alias": { + "dev-master": "6.0-dev" + } + }, "minimum-stability": "stable" } diff --git a/readme.md b/readme.md deleted file mode 100644 index 796a5156..00000000 --- a/readme.md +++ /dev/null @@ -1,95 +0,0 @@ -# Datatables Package for Laravel 4|5 - -[![Laravel 4.2|5.0|5.1|5.2](https://img.shields.io/badge/Laravel-4.2|5.0|5.1|5.2-orange.svg)](http://laravel.com) -[![Latest Stable Version](https://poser.pugx.org/yajra/laravel-datatables-oracle/v/stable)](https://packagist.org/packages/yajra/laravel-datatables-oracle) -[![Build Status](https://travis-ci.org/yajra/laravel-datatables.svg?branch=master)](https://travis-ci.org/yajra/laravel-datatables) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/yajra/laravel-datatables/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/yajra/laravel-datatables/?branch=master) -[![Total Downloads](https://poser.pugx.org/yajra/laravel-datatables-oracle/downloads)](https://packagist.org/packages/yajra/laravel-datatables-oracle) -[![License](https://poser.pugx.org/yajra/laravel-datatables-oracle/license)](https://packagist.org/packages/yajra/laravel-datatables-oracle) - -This package is created to handle [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. - -## Feature Overview -- Supports the following data source - - **Eloquent ORM** - - **Fluent Query Builder** - - **Collection** [available on v5.x and later] -- [DataTable Service Implementation (v6.x)](https://github.com/yajra/laravel-datatables/blob/6.0/CHANGELOG.md). -- Adding or editing content of columns and removing columns -- Templating new or current columns via Blade Template Engine or by using Closure -- Works with **ALL the DATABASE** supported by Laravel -- Works with **Oracle Database** using [Laravel-OCI8](https://github.com/yajra/laravel-oci8) package -- Works with [DataTables](http://datatables.net) v1.10++. - - **Note:** DT Legacy code is not supported on v5.x -- Works with [DataTables](http://datatables.net) v1.9 and v1.10 legacy code. - - **Note:** Use [v4.x](https://github.com/yajra/laravel-datatables-oracle/tree/v4.3.2) for Laravel 5 and [v3.x](https://github.com/yajra/laravel-datatables-oracle/tree/L4) for Laravel 4 -- Extended column filtering via [`filterColumn`](http://yajra.github.io/laravel-datatables/api/source-class-yajra.Datatables.Engines.BaseEngine.html#489-503) API. -- Extended column ordering via [`orderColumn`](http://yajra.github.io/laravel-datatables/api/source-class-yajra.Datatables.Engines.BaseEngine.html#505-519) API. -- Extended Query Builder functionality allowing you to filter using Datatables class directly. -- Decorate your data output using [`league\fractal`](https://github.com/thephpleague/fractal) Transformer with Serializer support. -- Works with Laravel Dependency Injection and IoC Container. -- Provides a [DataTable Html Builder](http://datatables.yajrabox.com/html) to help you use the package with less code. -- Provides XSS filtering function to optionally escape all or specified column values using `escapeColumns('*'\['column'])` method. -- Provides Query Logging when application is in debug state. **Important: Make sure that debug is set to false when your code is in production** - -## Requirements: -- PHP 5.5.9 or later. -- Laravel 5.0 or later. - -## Laravel 4.2 & DataTables v1.9.x Users -Most of the latest updates/features are not available on these versions. Please check [L4 Branch](https://github.com/yajra/laravel-datatables/tree/L4) and [L5 DT1.9](https://github.com/yajra/laravel-datatables/tree/L5-DT1.9) for old documentations of its features. - -## Buy me a beer -Click here to lend your support to: Laravel Datatables and make a donation at pledgie.com ! - -## Documentations -- You will find user friendly and updated documentation in the wiki here: [Laravel Datatables Wiki](https://github.com/yajra/laravel-datatables/wiki) -- You will find the API Documentation here: [Laravel Datatables API](http://yajra.github.io/laravel-datatables/api/) -- [Demo Application](http://datatables.yajrabox.com) is available for artisan's reference. - -## Quick Installation -**Laravel 5:** `composer require yajra/laravel-datatables-oracle:~6.0` - -**Laravel 4:** `composer require yajra/laravel-datatables-oracle:~3.0` - -#### Service Provider -`Yajra\Datatables\DatatablesServiceProvider` - -#### Facade -**Laravel 4** -`'Datatables' => 'yajra\Datatables\Facades\Datatables',` - -**Laravel 5++** -`Datatables` facade are automatically registered as an alias for `Yajra\Datatables\Datatables` class. - -#### Configuration -**Laravel 5:** `$ php artisan vendor:publish --tag=datatables` - -**Laravel 4:** `$ php artisan config:publish yajra/laravel-datatables-oracle` - -And that's it! Start building out some awesome DataTables! - -## Upgrading from v5.x to v6.x - - Change all occurrences of `yajra\Datatables` to `Yajra\Datatables`. (Use Sublime's find and replace all for faster update). - - Remove `Datatables` facade registration. - - Temporarily comment out `Yajra\Datatables\DatatablesServiceProvider`. - - Update package version on your composer.json and use `yajra/laravel-datatables-oracle: ~6.0` - - Uncomment the provider `Yajra\Datatables\DatatablesServiceProvider`. - -## Contributing - -Please see [CONTRIBUTING](https://github.com/yajra/laravel-datatables/blob/master/CONTRIBUTING.md) for details. - -## Security - -If you discover any security related issues, please email [aqangeles@gmail.com](mailto:aqangeles@gmail.com) instead of using the issue tracker. - -## Credits - -- This project is used to be a fork from [bllim/laravel4-datatables-package](https://github.com/bllim/laravel4-datatables-package). -- [All Contributors](https://github.com/yajra/laravel-datatables/graphs/contributors) - -## License - -The MIT License (MIT). Please see [License File](https://github.com/yajra/laravel-datatables/blob/master/LICENSE.md) for more information. - diff --git a/src/Contracts/DataTableButtonsContract.php b/src/Contracts/DataTableButtonsContract.php index 084d1f75..040f6e63 100644 --- a/src/Contracts/DataTableButtonsContract.php +++ b/src/Contracts/DataTableButtonsContract.php @@ -2,6 +2,12 @@ namespace Yajra\Datatables\Contracts; +/** + * Interface DataTableButtonsContract. + * + * @package Yajra\Datatables\Contracts + * @author Arjay Angeles + */ interface DataTableButtonsContract { /** diff --git a/src/Contracts/DataTableContract.php b/src/Contracts/DataTableContract.php index e692c67d..be474d77 100644 --- a/src/Contracts/DataTableContract.php +++ b/src/Contracts/DataTableContract.php @@ -2,6 +2,12 @@ namespace Yajra\Datatables\Contracts; +/** + * Interface DataTableContract + * + * @package Yajra\Datatables\Contracts + * @author Arjay Angeles + */ interface DataTableContract { /** @@ -35,7 +41,7 @@ public function builder(); public function request(); /** - * @return \Illuminate\Database\Eloquent\Builder + * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder|\Illuminate\Support\Collection */ public function query(); } diff --git a/src/Contracts/DataTableEngineContract.php b/src/Contracts/DataTableEngineContract.php index 33f03db0..31c673f7 100644 --- a/src/Contracts/DataTableEngineContract.php +++ b/src/Contracts/DataTableEngineContract.php @@ -2,65 +2,79 @@ namespace Yajra\Datatables\Contracts; +/** + * Interface DataTableEngineContract + * + * @package Yajra\Datatables\Contracts + * @author Arjay Angeles + */ interface DataTableEngineContract { /** - * Get results + * Get results. * * @return mixed */ public function results(); /** - * Count results + * Count results. * * @return integer */ public function count(); + /** + * Count total items. + * + * @return integer + */ + public function totalCount(); + /** * Set auto filter off and run your own filter. - * Overrides global search + * Overrides global search. * * @param \Closure $callback + * @param bool $globalSearch * @return $this */ - public function filter(\Closure $callback); + public function filter(\Closure $callback, $globalSearch = false); /** - * Perform global search + * Perform global search. * * @return void */ public function filtering(); /** - * Perform column search + * Perform column search. * * @return void */ public function columnSearch(); /** - * Perform pagination + * Perform pagination. * * @return void */ public function paging(); /** - * Perform sorting of columns + * Perform sorting of columns. * * @return void */ public function ordering(); - /** - * Organizes works + * Organizes works. * * @param bool $mDataSupport + * @param bool $orderFirst * @return \Illuminate\Http\JsonResponse */ - public function make($mDataSupport = false); + public function make($mDataSupport = false, $orderFirst = false); } diff --git a/src/Contracts/DataTableScopeContract.php b/src/Contracts/DataTableScopeContract.php index 12451f63..4ceaeea8 100644 --- a/src/Contracts/DataTableScopeContract.php +++ b/src/Contracts/DataTableScopeContract.php @@ -2,6 +2,12 @@ namespace Yajra\Datatables\Contracts; +/** + * Interface DataTableScopeContract. + * + * @package Yajra\Datatables\Contracts + * @author Arjay Angeles + */ interface DataTableScopeContract { /** diff --git a/src/Datatables.php b/src/Datatables.php index ec640a23..aa389281 100644 --- a/src/Datatables.php +++ b/src/Datatables.php @@ -2,15 +2,6 @@ namespace Yajra\Datatables; -/** - * Laravel Datatables Package - * This Package is created to handle server-side works of DataTables Jquery Plugin (http://datatables.net) - * - * @package Laravel - * @category Package - * @author Arjay Angeles - */ - use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Support\Collection; use Yajra\Datatables\Engines\CollectionEngine; @@ -18,12 +9,13 @@ use Yajra\Datatables\Engines\QueryBuilderEngine; /** - * Class Datatables + * Class Datatables. * * @package Yajra\Datatables * @method EloquentEngine eloquent($builder) * @method CollectionEngine collection(Collection $builder) * @method QueryBuilderEngine queryBuilder(QueryBuilder $builder) + * @author Arjay Angeles */ class Datatables { @@ -42,7 +34,7 @@ class Datatables public $builder; /** - * Class Constructor + * Datatables constructor. * * @param \Yajra\Datatables\Request $request */ @@ -52,14 +44,14 @@ public function __construct(Request $request) } /** - * Gets query and returns instance of class + * Gets query and returns instance of class. * * @param mixed $builder * @return mixed */ public static function of($builder) { - $datatables = app('datatables'); + $datatables = app(static::class); $datatables->builder = $builder; if ($builder instanceof QueryBuilder) { @@ -93,6 +85,17 @@ public function usingCollection(Collection $builder) return new CollectionEngine($builder, $this->request); } + /** + * Datatables using Eloquent. + * + * @param mixed $builder + * @return \Yajra\Datatables\Engines\EloquentEngine + */ + public function usingEloquent($builder) + { + return new EloquentEngine($builder, $this->request); + } + /** * Allows api call without the "using" word. * @@ -111,17 +114,6 @@ public function __call($name, $arguments) return trigger_error('Call to undefined method ' . __CLASS__ . '::' . $name . '()', E_USER_ERROR); } - /** - * Datatables using Eloquent - * - * @param mixed $builder - * @return \Yajra\Datatables\Engines\EloquentEngine - */ - public function usingEloquent($builder) - { - return new EloquentEngine($builder, $this->request); - } - /** * Get html builder class. * @@ -129,13 +121,13 @@ public function usingEloquent($builder) */ public function getHtmlBuilder() { - return app('Yajra\Datatables\Html\Builder'); + return app(\Yajra\Datatables\Html\Builder::class); } /** * Get request object. * - * @return \Yajra\Datatables\Request|static + * @return \Yajra\Datatables\Request */ public function getRequest() { diff --git a/src/DatatablesServiceProvider.php b/src/DatatablesServiceProvider.php index 3f1d1da8..e4f9cac8 100644 --- a/src/DatatablesServiceProvider.php +++ b/src/DatatablesServiceProvider.php @@ -4,10 +4,18 @@ use Collective\Html\HtmlServiceProvider; use Illuminate\Support\ServiceProvider; +use League\Fractal\Manager; +use League\Fractal\Serializer\DataArraySerializer; use Maatwebsite\Excel\ExcelServiceProvider; use Yajra\Datatables\Generators\DataTablesMakeCommand; use Yajra\Datatables\Generators\DataTablesScopeCommand; +/** + * Class DatatablesServiceProvider. + * + * @package Yajra\Datatables + * @author Arjay Angeles + */ class DatatablesServiceProvider extends ServiceProvider { /** @@ -34,7 +42,7 @@ public function boot() /** * Publish datatables assets. */ - private function publishAssets() + protected function publishAssets() { $this->publishes([ __DIR__ . '/config/config.php' => config_path('datatables.php'), @@ -52,7 +60,7 @@ private function publishAssets() /** * Register datatables commands. */ - private function registerCommands() + protected function registerCommands() { $this->commands(DataTablesMakeCommand::class); $this->commands(DataTablesScopeCommand::class); @@ -71,8 +79,26 @@ public function register() $this->registerRequiredProviders(); - $this->app->singleton('datatables', function ($app) { - return new Datatables($app->make(Request::class)); + $this->app->singleton('datatables', function () { + return new Datatables($this->app->make(Request::class)); + }); + + $this->app->alias('datatables', Datatables::class); + + $this->app->singleton('datatables.fractal', function () { + $fractal = new Manager; + $config = $this->app['config']; + $request = $this->app['request']; + + $includesKey = $config->get('datatables.fractal.includes', 'include'); + if ($request->get($includesKey)) { + $fractal->parseIncludes($request->get($includesKey)); + } + + $serializer = $config->get('datatables.fractal.serializer', DataArraySerializer::class); + $fractal->setSerializer(new $serializer); + + return $fractal; }); $this->registerAliases(); @@ -83,7 +109,7 @@ public function register() * * @return bool */ - private function isLumen() + protected function isLumen() { return str_contains($this->app->version(), 'Lumen'); } @@ -91,7 +117,7 @@ private function isLumen() /** * Register 3rd party providers. */ - private function registerRequiredProviders() + protected function registerRequiredProviders() { $this->app->register(HtmlServiceProvider::class); $this->app->register(ExcelServiceProvider::class); @@ -100,11 +126,11 @@ private function registerRequiredProviders() /** * Create aliases for the dependency. */ - private function registerAliases() + protected function registerAliases() { if (class_exists('Illuminate\Foundation\AliasLoader')) { $loader = \Illuminate\Foundation\AliasLoader::getInstance(); - $loader->alias('Datatables', \Yajra\Datatables\Datatables::class); + $loader->alias('Datatables', \Yajra\Datatables\Facades\Datatables::class); } } diff --git a/src/Engines/BaseEngine.php b/src/Engines/BaseEngine.php index 3419393d..b4dda368 100644 --- a/src/Engines/BaseEngine.php +++ b/src/Engines/BaseEngine.php @@ -2,24 +2,20 @@ namespace Yajra\Datatables\Engines; -/* - * Laravel Datatables Base Engine - * - * @package Laravel - * @category Package - * @author Arjay Angeles - */ - use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Config; use Illuminate\Support\Str; -use League\Fractal\Manager; use League\Fractal\Resource\Collection; -use League\Fractal\Serializer\DataArraySerializer; use Yajra\Datatables\Contracts\DataTableEngineContract; use Yajra\Datatables\Helper; use Yajra\Datatables\Processors\DataProcessor; +/** + * Class BaseEngine. + * + * @package Yajra\Datatables\Engines + * @author Arjay Angeles + */ abstract class BaseEngine implements DataTableEngineContract { /** @@ -39,7 +35,7 @@ abstract class BaseEngine implements DataTableEngineContract /** * Builder object. * - * @var mixed + * @var \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder */ protected $query; @@ -63,12 +59,15 @@ abstract class BaseEngine implements DataTableEngineContract * @var array */ protected $columnDef = [ - 'append' => [], - 'edit' => [], - 'excess' => ['rn', 'row_num'], - 'filter' => [], - 'order' => [], - 'escape' => [], + 'index' => false, + 'append' => [], + 'edit' => [], + 'excess' => ['rn', 'row_num'], + 'filter' => [], + 'order' => [], + 'escape' => [], + 'blacklist' => ['password', 'remember_token'], + 'whitelist' => '*', ]; /** @@ -109,7 +108,7 @@ abstract class BaseEngine implements DataTableEngineContract /** * Callback to override global search. * - * @var \Closure + * @var callable */ protected $filterCallback; @@ -163,9 +162,23 @@ abstract class BaseEngine implements DataTableEngineContract /** * Fractal serializer class. * - * @var string + * @var string|null + */ + protected $serializer = null; + + /** + * Custom ordering callback. + * + * @var callable */ - protected $serializer; + protected $orderCallback; + + /** + * Skip paginate as needed. + * + * @var bool + */ + protected $skipPaging = false; /** * Array of data to append on json response. @@ -182,14 +195,28 @@ abstract class BaseEngine implements DataTableEngineContract */ public function setupKeyword($value) { - $keyword = '%' . $value . '%'; - if ($this->isWildcard()) { - $keyword = $this->wildcardLikeString($value); + if ($this->isSmartSearch()) { + $keyword = '%' . $value . '%'; + if ($this->isWildcard()) { + $keyword = $this->wildcardLikeString($value); + } + // remove escaping slash added on js script request + $keyword = str_replace('\\', '%', $keyword); + + return $keyword; } - // remove escaping slash added on js script request - $keyword = str_replace('\\', '%', $keyword); - return $keyword; + return $value; + } + + /** + * Check if DataTables uses smart search. + * + * @return bool + */ + protected function isSmartSearch() + { + return Config::get('datatables.search.smart', true); } /** @@ -212,12 +239,14 @@ public function isWildcard() public function wildcardLikeString($str, $lowercase = true) { $wild = '%'; - $length = strlen($str); - if ($length) { - for ($i = 0; $i < $length; $i++) { - $wild .= $str[$i] . '%'; + $chars = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY); + + if (count($chars) > 0) { + foreach ($chars as $char) { + $wild .= $char . '%'; } } + if ($lowercase) { $wild = Str::lower($wild); } @@ -225,62 +254,6 @@ public function wildcardLikeString($str, $lowercase = true) return $wild; } - /** - * Setup column name to be use for filtering. - * - * @param integer $i - * @param bool $wantsAlias - * @return string - */ - public function setupColumnName($i, $wantsAlias = false) - { - $column = $this->getColumnName($i); - if (Str::contains(Str::upper($column), ' AS ')) { - $column = $this->extractColumnName($column, $wantsAlias); - } - - return $column; - } - - /** - * Get column name by order column index. - * - * @param int $column - * @return mixed - */ - protected function getColumnName($column) - { - $name = $this->request->columnName($column) ?: (isset($this->columns[$column]) ? $this->columns[$column] : $this->columns[0]); - - return in_array($name, $this->extraColumns, true) ? $this->columns[0] : $name; - } - - /** - * Get column name from string. - * - * @param string $str - * @param bool $wantsAlias - * @return string - */ - public function extractColumnName($str, $wantsAlias) - { - $matches = explode(' as ', Str::lower($str)); - - if (! empty($matches)) { - if ($wantsAlias) { - return array_pop($matches); - } else { - return array_shift($matches); - } - } elseif (strpos($str, '.')) { - $array = explode('.', $str); - - return array_pop($array); - } - - return $str; - } - /** * Will prefix column if needed. * @@ -358,7 +331,7 @@ public function isQueryBuilder() * Add column in collection. * * @param string $name - * @param string $content + * @param string|callable $content * @param bool|int $order * @return $this */ @@ -371,11 +344,23 @@ public function addColumn($name, $content, $order = false) return $this; } + /** + * Add DT row index column on response. + * + * @return $this + */ + public function addIndexColumn() + { + $this->columnDef['index'] = true; + + return $this; + } + /** * Edit column's content. * * @param string $name - * @param string $content + * @param string|callable $content * @return $this */ public function editColumn($name, $content) @@ -414,10 +399,11 @@ public function escapeColumns($columns = '*') /** * Allows previous API calls where the methods were snake_case. * Will convert a camelCase API call to a snake_case call. + * Allow query builder method to be used by the engine. * - * @param $name - * @param $arguments - * @return $this|mixed + * @param string $name + * @param array $arguments + * @return mixed */ public function __call($name, $arguments) { @@ -434,7 +420,7 @@ public function __call($name, $arguments) } /** - * Sets DT_RowClass template + * Sets DT_RowClass template. * result: . * * @param string|callable $content @@ -448,7 +434,7 @@ public function setRowClass($content) } /** - * Sets DT_RowId template + * Sets DT_RowId template. * result: . * * @param string|callable $content @@ -489,7 +475,7 @@ public function addRowData($key, $value) } /** - * Set DT_RowAttr templates + * Set DT_RowAttr templates. * result: . * * @param array $data @@ -520,7 +506,7 @@ public function addRowAttr($key, $value) * Override default column filter search. * * @param string $column - * @param string $method + * @param string|callable $method * @return $this * @internal param $mixed ...,... All the individual parameters required for specified $method * @internal string $1 Special variable that returns the requested search keyword. @@ -533,6 +519,23 @@ public function filterColumn($column, $method) return $this; } + /** + * Order each given columns versus the given custom sql. + * + * @param array $columns + * @param string $sql + * @param array $bindings + * @return $this + */ + public function orderColumns(array $columns, $sql, $bindings = []) + { + foreach ($columns as $column) { + $this->orderColumn($column, str_replace(':column', $column, $sql), $bindings); + } + + return $this; + } + /** * Override default column ordering. * @@ -596,13 +599,6 @@ public function make($mDataSupport = false, $orderFirst = false) return $this->render($mDataSupport); } - /** - * Count total items. - * - * @return integer - */ - abstract public function totalCount(); - /** * Sort records. * @@ -616,13 +612,6 @@ public function orderRecords($skip) } } - /** - * Perform sorting of columns. - * - * @return void - */ - abstract public function ordering(); - /** * Perform necessary filters. * @@ -632,37 +621,16 @@ public function filterRecords() { if ($this->autoFilter && $this->request->isSearchable()) { $this->filtering(); - } else { - if (is_callable($this->filterCallback)) { - call_user_func($this->filterCallback, $this->filterCallbackParameters); - } + } + + if (is_callable($this->filterCallback)) { + call_user_func($this->filterCallback, $this->filterCallbackParameters); } $this->columnSearch(); $this->filteredRecords = $this->isFilterApplied ? $this->count() : $this->totalRecords; } - /** - * Perform global search. - * - * @return void - */ - abstract public function filtering(); - - /** - * Perform column search. - * - * @return void - */ - abstract public function columnSearch(); - - /** - * Count results. - * - * @return integer - */ - abstract public function count(); - /** * Apply pagination. * @@ -670,18 +638,11 @@ abstract public function count(); */ public function paginate() { - if ($this->request->isPaginationable()) { + if ($this->request->isPaginationable() && ! $this->skipPaging) { $this->paging(); } } - /** - * Perform pagination - * - * @return void - */ - abstract public function paging(); - /** * Render json response. * @@ -697,13 +658,11 @@ public function render($object = false) ], $this->appends); if (isset($this->transformer)) { - $fractal = new Manager(); - if ($this->request->get('include')) { - $fractal->parseIncludes($this->request->get('include')); - } + $fractal = app('datatables.fractal'); - $serializer = $this->serializer ?: Config::get('datatables.fractal.serializer', DataArraySerializer::class); - $fractal->setSerializer(new $serializer); + if ($this->serializer) { + $fractal->setSerializer($this->createSerializer()); + } //Get transformer reflection //Firs method parameter should be data/object to transform @@ -713,11 +672,11 @@ public function render($object = false) //If parameter is class assuming it requires object //Else just pass array by default if ($parameter->getClass()) { - $resource = new Collection($this->results(), new $this->transformer()); + $resource = new Collection($this->results(), $this->createTransformer()); } else { $resource = new Collection( $this->getProcessedData($object), - new $this->transformer() + $this->createTransformer() ); } @@ -731,15 +690,36 @@ public function render($object = false) $output = $this->showDebugger($output); } - return new JsonResponse($output); + return new JsonResponse($output, 200, Config::get('datatables.json.header', []), Config::get('datatables.json.options', 0)); } /** - * Get results + * Get or create transformer serializer instance. * - * @return array + * @return \League\Fractal\Serializer\SerializerAbstract + */ + protected function createSerializer() + { + if ($this->serializer instanceof \League\Fractal\Serializer\SerializerAbstract) { + return $this->serializer; + } + + return new $this->serializer(); + } + + /** + * Get or create transformer instance. + * + * @return \League\Fractal\TransformerAbstract */ - abstract public function results(); + protected function createTransformer() + { + if ($this->transformer instanceof \League\Fractal\TransformerAbstract) { + return $this->transformer; + } + + return new $this->transformer(); + } /** * Get processed data @@ -752,7 +732,8 @@ private function getProcessedData($object = false) $processor = new DataProcessor( $this->results(), $this->columnDef, - $this->templates + $this->templates, + $this->request['start'] ); return $processor->process($object); @@ -782,25 +763,16 @@ public function showDebugger(array $output) return $output; } - /** - * Set auto filter off and run your own filter. - * Overrides global search - * - * @param \Closure $callback - * @return $this - */ - abstract public function filter(\Closure $callback); - /** * Update flags to disable global search * - * @param \Closure $callback + * @param callable $callback * @param mixed $parameters - * @return void + * @param bool $autoFilter */ - public function overrideGlobalSearch(\Closure $callback, $parameters) + public function overrideGlobalSearch(callable $callback, $parameters, $autoFilter = false) { - $this->autoFilter = false; + $this->autoFilter = $autoFilter; $this->isFilterApplied = true; $this->filterCallback = $callback; $this->filterCallbackParameters = $parameters; @@ -835,4 +807,194 @@ public function with($key, $value = '') return $this; } + + /** + * Override default ordering method with a closure callback. + * + * @param callable $closure + * @return $this + */ + public function order(callable $closure) + { + $this->orderCallback = $closure; + + return $this; + } + + /** + * Update list of columns that is not allowed for search/sort. + * + * @param array $blacklist + * @return $this + */ + public function blacklist(array $blacklist) + { + $this->columnDef['blacklist'] = $blacklist; + + return $this; + } + + /** + * Update list of columns that is not allowed for search/sort. + * + * @param string|array $whitelist + * @return $this + */ + public function whitelist($whitelist = '*') + { + $this->columnDef['whitelist'] = $whitelist; + + return $this; + } + + /** + * Set smart search config at runtime. + * + * @param bool $bool + * @return $this + */ + public function smart($bool = true) + { + Config::set('datatables.search.smart', $bool); + + return $this; + } + + /** + * Set total records manually. + * + * @param int $total + * @return $this + */ + public function setTotalRecords($total) + { + $this->totalRecords = $total; + + return $this; + } + + /** + * Skip pagination as needed. + */ + public function skipPaging() + { + $this->skipPaging = true; + + return $this; + } + + /** + * Check if column is blacklisted. + * + * @param string $column + * @return bool + */ + protected function isBlacklisted($column) + { + if (in_array($column, $this->columnDef['blacklist'])) { + return true; + } + + if ($this->columnDef['whitelist'] === '*' || in_array($column, $this->columnDef['whitelist'])) { + return false; + } + + return true; + } + + /** + * Get column name to be use for filtering and sorting. + * + * @param integer $index + * @param bool $wantsAlias + * @return string + */ + protected function getColumnName($index, $wantsAlias = false) + { + $column = $this->request->columnName($index); + + // DataTables is using make(false) + if (is_numeric($column)) { + $column = $this->getColumnNameByIndex($index); + } + + if (Str::contains(Str::upper($column), ' AS ')) { + $column = $this->extractColumnName($column, $wantsAlias); + } + + return $column; + } + + /** + * Get column name by order column index. + * + * @param int $index + * @return mixed + */ + protected function getColumnNameByIndex($index) + { + $name = isset($this->columns[$index]) && $this->columns[$index] <> '*' ? $this->columns[$index] : $this->getPrimaryKeyName(); + + return in_array($name, $this->extraColumns, true) ? $this->getPrimaryKeyName() : $name; + } + + /** + * If column name could not be resolved then use primary key. + * + * @return string + */ + protected function getPrimaryKeyName() + { + if ($this->isEloquent()) { + return $this->query->getModel()->getKeyName(); + } + + return 'id'; + } + + /** + * Check if the engine used was eloquent. + * + * @return bool + */ + protected function isEloquent() + { + return $this->query_type === 'eloquent'; + } + + /** + * Get column name from string. + * + * @param string $str + * @param bool $wantsAlias + * @return string + */ + protected function extractColumnName($str, $wantsAlias) + { + $matches = explode(' as ', Str::lower($str)); + + if (! empty($matches)) { + if ($wantsAlias) { + return array_pop($matches); + } else { + return array_shift($matches); + } + } elseif (strpos($str, '.')) { + $array = explode('.', $str); + + return array_pop($array); + } + + return $str; + } + + /** + * Check if the current sql language is based on oracle syntax. + * + * @return bool + */ + protected function isOracleSql() + { + return in_array($this->database, ['oracle', 'oci8']); + } } diff --git a/src/Engines/CollectionEngine.php b/src/Engines/CollectionEngine.php index 9269c202..d27f0674 100644 --- a/src/Engines/CollectionEngine.php +++ b/src/Engines/CollectionEngine.php @@ -2,40 +2,39 @@ namespace Yajra\Datatables\Engines; -/** - * Laravel Datatables Collection Engine - * - * @package Laravel - * @category Package - * @author Arjay Angeles - */ - use Closure; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; -use Yajra\Datatables\Contracts\DataTableEngineContract; use Yajra\Datatables\Request; -class CollectionEngine extends BaseEngine implements DataTableEngineContract +/** + * Class CollectionEngine. + * + * @package Yajra\Datatables\Engines + * @author Arjay Angeles + */ +class CollectionEngine extends BaseEngine { /** * Collection object * - * @var Collection + * @var \Illuminate\Support\Collection */ public $collection; /** * Collection object * - * @var Collection + * @var \Illuminate\Support\Collection */ public $original_collection; /** - * @param Collection $collection + * CollectionEngine constructor. + * + * @param \Illuminate\Support\Collection $collection * @param \Yajra\Datatables\Request $request */ public function __construct(Collection $collection, Request $request) @@ -58,17 +57,25 @@ protected function serialize($collection) } /** - * @inheritdoc + * Set auto filter off and run your own filter. + * Overrides global search. + * + * @param \Closure $callback + * @param bool $globalSearch + * @return $this */ - public function filter(Closure $callback) + public function filter(Closure $callback, $globalSearch = false) { - $this->overrideGlobalSearch($callback, $this); + $this->overrideGlobalSearch($callback, $this, $globalSearch); return $this; } /** - * @inheritdoc + * Append debug parameters on output. + * + * @param array $output + * @return array */ public function showDebugger(array $output) { @@ -78,35 +85,51 @@ public function showDebugger(array $output) } /** - * @inheritdoc + * Count total items. + * + * @return integer */ public function totalCount() { - return $this->count(); + return $this->totalRecords ? $this->totalRecords : $this->collection->count(); } /** - * @inheritdoc + * Count results. + * + * @return integer */ public function count() { - return $this->collection->count(); + return $this->collection->count() > $this->totalRecords ? $this->totalRecords : $this->collection->count(); } /** - * @inheritdoc + * Perform sorting of columns. + * + * @return void */ public function ordering() { + if ($this->orderCallback) { + call_user_func($this->orderCallback, $this); + + return; + } + foreach ($this->request->orderableColumns() as $orderable) { - $column = $this->getColumnName($orderable['column']); - $this->collection = $this->collection->sortBy( - function ($row) use ($column) { - $data = $this->serialize($row); + $column = $this->getColumnName($orderable['column']); - return Arr::get($data, $column); - } - ); + $options = SORT_NATURAL; + if ($this->isCaseInsensitive()) { + $options = SORT_NATURAL | SORT_FLAG_CASE; + } + + $this->collection = $this->collection->sortBy(function ($row) use ($column) { + $data = $this->serialize($row); + + return Arr::get($data, $column); + }, $options); if ($orderable['direction'] == 'desc') { $this->collection = $this->collection->reverse(); @@ -115,7 +138,9 @@ function ($row) use ($column) { } /** - * @inheritdoc + * Perform global search. + * + * @return void */ public function filtering() { @@ -134,7 +159,15 @@ function ($row) use ($columns) { } if ($this->isCaseInsensitive()) { - $found[] = Str::contains(Str::lower($value), Str::lower($keyword)); + + if(is_array($value)){ + foreach ($value as $v) { + $found[] = Str::contains(Str::lower($v), Str::lower($keyword)); + }; + } else{ + $found[] = Str::contains(Str::lower($value), Str::lower($keyword)); + } + } else { $found[] = Str::contains($value, $keyword); } @@ -146,7 +179,9 @@ function ($row) use ($columns) { } /** - * @inheritdoc + * Perform column search. + * + * @return void */ public function columnSearch() { @@ -154,17 +189,29 @@ public function columnSearch() for ($i = 0, $c = count($columns); $i < $c; $i++) { if ($this->request->isColumnSearchable($i)) { $this->isFilterApplied = true; + $regex = $this->request->isRegex($i); $column = $this->getColumnName($i); $keyword = $this->request->columnKeyword($i); $this->collection = $this->collection->filter( - function ($row) use ($column, $keyword) { + function ($row) use ($column, $keyword, $regex) { $data = $this->serialize($row); + + $value = Arr::get($data, $column); + if ($this->isCaseInsensitive()) { - return strpos(Str::lower($data[$column]), Str::lower($keyword)) !== false; + if ($regex) { + return preg_match('/' . $keyword . '/i', $value) == 1; + } else { + return strpos(Str::lower($value), Str::lower($keyword)) !== false; + } } else { - return strpos($data[$column], $keyword) !== false; + if ($regex) { + return preg_match('/' . $keyword . '/', $value) == 1; + } else { + return strpos($value, $keyword) !== false; + } } } ); @@ -173,18 +220,22 @@ function ($row) use ($column, $keyword) { } /** - * @inheritdoc + * Perform pagination. + * + * @return void */ public function paging() { $this->collection = $this->collection->slice( $this->request['start'], - (int) $this->request['length'] > 0 ? $this->request['length'] : 10 + (int)$this->request['length'] > 0 ? $this->request['length'] : 10 ); } /** - * @inheritdoc + * Get results. + * + * @return mixed */ public function results() { @@ -192,7 +243,11 @@ public function results() } /** - * @inheritdoc + * Organizes works. + * + * @param bool $mDataSupport + * @param bool $orderFirst + * @return \Illuminate\Http\JsonResponse */ public function make($mDataSupport = false, $orderFirst = true) { diff --git a/src/Engines/EloquentEngine.php b/src/Engines/EloquentEngine.php index 7de02a05..dde4ab64 100644 --- a/src/Engines/EloquentEngine.php +++ b/src/Engines/EloquentEngine.php @@ -2,27 +2,112 @@ namespace Yajra\Datatables\Engines; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Str; +use Yajra\Datatables\Request; + /** - * Laravel Datatables Eloquent Engine + * Class EloquentEngine. * - * @package Laravel - * @category Package - * @author Arjay Angeles + * @package Yajra\Datatables\Engines + * @author Arjay Angeles */ +class EloquentEngine extends QueryBuilderEngine +{ + /** + * Select trashed records in count function for models with soft deletes trait. + * By default we do not select soft deleted records + * + * @var bool + */ + protected $withTrashed = false; -use Illuminate\Database\Eloquent\Builder; -use Yajra\Datatables\Contracts\DataTableEngineContract; -use Yajra\Datatables\Request; + /** + * Select only trashed records in count function for models with soft deletes trait. + * By default we do not select soft deleted records + * + * @var bool + */ + protected $onlyTrashed = false; -class EloquentEngine extends QueryBuilderEngine implements DataTableEngineContract -{ /** * @param mixed $model * @param \Yajra\Datatables\Request $request */ public function __construct($model, Request $request) { - $this->query = $model instanceof Builder ? $model : $model->getQuery(); - $this->init($request, $this->query->getQuery(), 'eloquent'); + $builder = $model instanceof Builder ? $model : $model->getQuery(); + parent::__construct($builder->getQuery(), $request); + + $this->query = $builder; + $this->query_type = 'eloquent'; + } + + /** + * Counts current query. + * + * @return int + */ + public function count() + { + $myQuery = clone $this->query; + // if its a normal query ( no union, having and distinct word ) + // replace the select with static text to improve performance + if (! Str::contains(Str::lower($myQuery->toSql()), ['union', 'having', 'distinct', 'order by', 'group by'])) { + $row_count = $this->wrap('row_count'); + $myQuery->select($this->connection->raw("'1' as {$row_count}")); + } + + // check for select soft deleted records + if (! $this->withTrashed && ! $this->onlyTrashed && $this->modelUseSoftDeletes()) { + $myQuery->whereNull($myQuery->getModel()->getQualifiedDeletedAtColumn()); + } + + if ($this->onlyTrashed && $this->modelUseSoftDeletes()) { + $myQuery->whereNotNull($myQuery->getModel()->getQualifiedDeletedAtColumn()); + } + + return $this->connection->table($this->connection->raw('(' . $myQuery->toSql() . ') count_row_table')) + ->setBindings($myQuery->getBindings())->count(); + } + + /** + * Check if model use SoftDeletes trait + * + * @return boolean + */ + protected function modelUseSoftDeletes() + { + if ($this->query_type == 'eloquent') { + return in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses($this->query->getModel())); + } + + return false; + } + + /** + * Change withTrashed flag value. + * + * @param bool $withTrashed + * @return $this + */ + public function withTrashed($withTrashed = true) + { + $this->withTrashed = $withTrashed; + + return $this; + } + + /** + * Change onlyTrashed flag value. + * + * @param bool $onlyTrashed + * @return $this + */ + public function onlyTrashed($onlyTrashed = true) + { + $this->onlyTrashed = $onlyTrashed; + + return $this; } } diff --git a/src/Engines/QueryBuilderEngine.php b/src/Engines/QueryBuilderEngine.php index c95af096..d824e5b0 100644 --- a/src/Engines/QueryBuilderEngine.php +++ b/src/Engines/QueryBuilderEngine.php @@ -2,22 +2,23 @@ namespace Yajra\Datatables\Engines; -/** - * Laravel Datatables Query Builder Engine - * - * @package Laravel - * @category Package - * @author Arjay Angeles - */ - use Closure; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasOneOrMany; +use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Query\Builder; +use Illuminate\Database\Query\Expression; use Illuminate\Support\Str; -use Yajra\Datatables\Contracts\DataTableEngineContract; use Yajra\Datatables\Helper; use Yajra\Datatables\Request; -class QueryBuilderEngine extends BaseEngine implements DataTableEngineContract +/** + * Class QueryBuilderEngine. + * + * @package Yajra\Datatables\Engines + * @author Arjay Angeles + */ +class QueryBuilderEngine extends BaseEngine { /** * @param \Illuminate\Database\Query\Builder $builder @@ -50,17 +51,26 @@ protected function init($request, $builder, $type = 'builder') } /** - * @inheritdoc + * Set auto filter off and run your own filter. + * Overrides global search + * + * @param \Closure $callback + * @param bool $globalSearch + * @return $this */ - public function filter(Closure $callback) + public function filter(Closure $callback, $globalSearch = false) { - $this->overrideGlobalSearch($callback, $this->query); + $this->overrideGlobalSearch($callback, $this->query, $globalSearch); return $this; } /** - * @inheritdoc + * Organizes works + * + * @param bool $mDataSupport + * @param bool $orderFirst + * @return \Illuminate\Http\JsonResponse */ public function make($mDataSupport = false, $orderFirst = false) { @@ -68,11 +78,13 @@ public function make($mDataSupport = false, $orderFirst = false) } /** - * @inheritdoc + * Count total items. + * + * @return integer */ public function totalCount() { - return $this->count(); + return $this->totalRecords ? $this->totalRecords : $this->count(); } /** @@ -85,8 +97,8 @@ public function count() $myQuery = clone $this->query; // if its a normal query ( no union, having and distinct word ) // replace the select with static text to improve performance - if (! Str::contains(Str::lower($myQuery->toSql()), ['union', 'having', 'distinct'])) { - $row_count = $this->connection->getQueryGrammar()->wrap('row_count'); + if (! Str::contains(Str::lower($myQuery->toSql()), ['union', 'having', 'distinct', 'order by', 'group by'])) { + $row_count = $this->wrap('row_count'); $myQuery->select($this->connection->raw("'1' as {$row_count}")); } @@ -95,24 +107,75 @@ public function count() } /** - * @inheritdoc + * Wrap column with DB grammar. + * + * @param string $column + * @return string + */ + protected function wrap($column) + { + return $this->connection->getQueryGrammar()->wrap($column); + } + + /** + * Perform global search. + * + * @return void */ public function filtering() { $this->query->where( function ($query) { - $keyword = $this->setupKeyword($this->request->keyword()); + $globalKeyword = $this->request->keyword(); + $queryBuilder = $this->getQueryBuilder($query); + foreach ($this->request->searchableColumnIndex() as $index) { - $columnName = $this->setupColumnName($index); + $columnName = $this->getColumnName($index); + if ($this->isBlacklisted($columnName)) { + continue; + } + // check if custom column filtering is applied if (isset($this->columnDef['filter'][$columnName])) { - $method = Helper::getOrMethod($this->columnDef['filter'][$columnName]['method']); - $parameters = $this->columnDef['filter'][$columnName]['parameters']; - $this->compileColumnQuery( - $this->getQueryBuilder($query), $method, $parameters, $columnName, $keyword - ); + $columnDef = $this->columnDef['filter'][$columnName]; + // check if global search should be applied for the specific column + $applyGlobalSearch = count($columnDef['parameters']) == 0 || end($columnDef['parameters']) !== false; + if (! $applyGlobalSearch) { + continue; + } + + if ($columnDef['method'] instanceof Closure) { + $whereQuery = $queryBuilder->newQuery(); + call_user_func_array($columnDef['method'], [$whereQuery, $globalKeyword]); + $queryBuilder->addNestedWhereQuery($whereQuery, 'or'); + } else { + $this->compileColumnQuery( + $queryBuilder, + Helper::getOrMethod($columnDef['method']), + $columnDef['parameters'], + $columnName, + $globalKeyword + ); + } } else { - $this->compileGlobalSearch($this->getQueryBuilder($query), $columnName, $keyword); + if (count(explode('.', $columnName)) > 1) { + $eagerLoads = $this->getEagerLoads(); + $parts = explode('.', $columnName); + $relationColumn = array_pop($parts); + $relation = implode('.', $parts); + if (in_array($relation, $eagerLoads)) { + $this->compileRelationSearch( + $queryBuilder, + $relation, + $relationColumn, + $globalKeyword + ); + } else { + $this->compileQuerySearch($queryBuilder, $columnName, $globalKeyword); + } + } else { + $this->compileQuerySearch($queryBuilder, $columnName, $globalKeyword); + } } $this->isFilterApplied = true; @@ -125,7 +188,7 @@ function ($query) { * Perform filter column on selected field. * * @param mixed $query - * @param string $method + * @param string|Closure $method * @param mixed $parameters * @param string $column * @param string $keyword @@ -167,75 +230,258 @@ protected function parameterize() } /** - * Add a query on global search. + * Get eager loads keys if eloquent. + * + * @return array + */ + protected function getEagerLoads() + { + if ($this->query_type == 'eloquent') { + return array_keys($this->query->getEagerLoads()); + } + + return []; + } + + /** + * Add relation query on global search. * * @param mixed $query + * @param string $relation * @param string $column * @param string $keyword */ - protected function compileGlobalSearch($query, $column, $keyword) + protected function compileRelationSearch($query, $relation, $column, $keyword) { + $myQuery = clone $this->query; + + /** + * For compile nested relation, we need store all nested relation as array + * and reverse order to apply where query. + * With this method we can create nested sub query with properly relation. + */ + + /** + * Store all relation data that require in next step + */ + $relationChunk = []; + + /** + * Store last eloquent query builder for get next relation. + */ + $lastQuery = $query; + + $relations = explode('.', $relation); + $lastRelation = end($relations); + foreach ($relations as $relation) { + $relationType = $myQuery->getModel()->{$relation}(); + $myQuery->orWhereHas($relation, function ($builder) use ( + $column, + $keyword, + $query, + $relationType, + $relation, + $lastRelation, + &$relationChunk, + &$lastQuery + ) { + $builder->select($this->connection->raw('count(1)')); + + // We will perform search on last relation only. + if ($relation == $lastRelation) { + $this->compileQuerySearch($builder, $column, $keyword, ''); + } + + // Put require object to next step!! + $relationChunk[$relation] = [ + 'builder' => $builder, + 'relationType' => $relationType, + 'query' => $lastQuery, + ]; + + // This is trick make sub query. + $lastQuery = $builder; + }); + + // This is trick to make nested relation by pass previous relation to be next query eloquent builder + $myQuery = $relationType; + } + + /** + * Reverse them all + */ + $relationChunk = array_reverse($relationChunk, true); + + /** + * Create valuable for use in check last relation + */ + end($relationChunk); + $lastRelation = key($relationChunk); + reset($relationChunk); + + /** + * Walking ... + */ + foreach ($relationChunk as $relation => $chunk) { + // Prepare variables + $builder = $chunk['builder']; + $query = $chunk['query']; + $bindings = $builder->getBindings(); + $sql = "({$builder->toSql()}) >= 1"; + + // Check if it last relation we will use orWhereRaw + if ($lastRelation == $relation) { + $relationMethod = "orWhereRaw"; + } else { + // For case parent relation of nested relation. + // We must use and for properly query and get correct result + $relationMethod = "whereRaw"; + } + + $query->{$relationMethod}($sql, $bindings); + } + } + + /** + * Compile query builder where clause depending on configurations. + * + * @param mixed $query + * @param string $column + * @param string $keyword + * @param string $relation + */ + protected function compileQuerySearch($query, $column, $keyword, $relation = 'or') + { + $column = $this->addTablePrefix($query, $column); $column = $this->castColumn($column); $sql = $column . ' LIKE ?'; + if ($this->isCaseInsensitive()) { - $sql = 'LOWER(' . $column . ') LIKE ?'; - $keyword = Str::lower($keyword); + $sql = 'LOWER(' . $column . ') LIKE ?'; + } + + $query->{$relation . 'WhereRaw'}($sql, [$this->prepareKeyword($keyword)]); + } + + /** + * Patch for fix about ambiguous field. + * Ambiguous field error will appear when query use join table and search with keyword. + * + * @param mixed $query + * @param string $column + * @return string + */ + protected function addTablePrefix($query, $column) + { + // Check if field does not have a table prefix + if (strpos($column, '.') === false) { + // Alternative method to check instanceof \Illuminate\Database\Eloquent\Builder + if (method_exists($query, 'getQuery')) { + $q = $query->getQuery(); + } else { + $q = $query; + } + + if (! $q->from instanceof Expression) { + // Get table from query and add it. + $column = $q->from . '.' . $column; + } } - $query->orWhereRaw($sql, [$keyword]); + return $this->wrap($column); } /** - * Wrap a column and cast in pgsql + * Wrap a column and cast in pgsql. * * @param string $column * @return string */ - public function castColumn($column) + protected function castColumn($column) { - $column = $this->connection->getQueryGrammar()->wrap($column); if ($this->database === 'pgsql') { $column = 'CAST(' . $column . ' as TEXT)'; + } elseif ($this->database === 'firebird') { + $column = 'CAST(' . $column . ' as VARCHAR(255))'; } return $column; } /** - * @inheritdoc + * Prepare search keyword based on configurations. + * + * @param string $keyword + * @return string + */ + protected function prepareKeyword($keyword) + { + if ($this->isCaseInsensitive()) { + $keyword = Str::lower($keyword); + } + + if ($this->isWildcard()) { + $keyword = $this->wildcardLikeString($keyword); + } + + if ($this->isSmartSearch()) { + $keyword = "%$keyword%"; + } + + return $keyword; + } + + /** + * Perform column search. + * + * @return void */ public function columnSearch() { - $columns = $this->request->get('columns'); - for ($i = 0, $c = count($columns); $i < $c; $i++) { - if ($this->request->isColumnSearchable($i)) { - $column = $this->setupColumnName($i); - $keyword = $this->getSearchKeyword($i); - - if (isset($this->columnDef['filter'][$column])) { - $method = $this->columnDef['filter'][$column]['method']; - $parameters = $this->columnDef['filter'][$column]['parameters']; - $this->compileColumnQuery($this->getQueryBuilder(), $method, $parameters, $column, $keyword); + $columns = (array) $this->request->input('columns'); + + foreach ($columns as $index => $column) { + if (! $this->request->isColumnSearchable($index)) { + continue; + } + + $column = $this->getColumnName($index); + + if (isset($this->columnDef['filter'][$column])) { + $columnDef = $this->columnDef['filter'][$column]; + // get a raw keyword (without wildcards) + $keyword = $this->getSearchKeyword($index, true); + $builder = $this->getQueryBuilder(); + + if ($columnDef['method'] instanceof Closure) { + $whereQuery = $builder->newQuery(); + call_user_func_array($columnDef['method'], [$whereQuery, $keyword]); + $builder->addNestedWhereQuery($whereQuery); } else { - $column = $this->castColumn($column); - if ($this->isCaseInsensitive()) { - if ($this->request->isRegex($i)) { - $this->query->whereRaw('LOWER(' . $column . ') REGEXP ?', [Str::lower($keyword)]); - } else { - $this->query->whereRaw('LOWER(' . $column . ') LIKE ?', [Str::lower($keyword)]); - } - } else { - $col = strstr($column, '(') ? $this->connection->raw($column) : $column; - if ($this->request->isRegex($i)) { - $this->query->whereRaw($col . ' REGEXP ?', [$keyword]); - } else { - $this->query->whereRaw($col . ' LIKE ?', [$keyword]); - } + $this->compileColumnQuery( + $builder, + $columnDef['method'], + $columnDef['parameters'], + $column, + $keyword + ); + } + } else { + if (count(explode('.', $column)) > 1) { + $eagerLoads = $this->getEagerLoads(); + $parts = explode('.', $column); + $relationColumn = array_pop($parts); + $relation = implode('.', $parts); + if (in_array($relation, $eagerLoads)) { + $column = $this->joinEagerLoadedColumn($relation, $relationColumn); } } - $this->isFilterApplied = true; + $keyword = $this->getSearchKeyword($index); + $this->compileColumnSearch($index, $column, $keyword); } + + $this->isFilterApplied = true; } } @@ -243,71 +489,190 @@ public function columnSearch() * Get proper keyword to use for search. * * @param int $i + * @param bool $raw * @return string */ - private function getSearchKeyword($i) + private function getSearchKeyword($i, $raw = false) { - if ($this->request->isRegex($i)) { - return $this->request->columnKeyword($i); + $keyword = $this->request->columnKeyword($i); + if ($raw || $this->request->isRegex($i)) { + return $keyword; } - return $this->setupKeyword($this->request->columnKeyword($i)); + return $this->setupKeyword($keyword); } /** - * @inheritdoc + * Join eager loaded relation and get the related column name. + * + * @param string $relation + * @param string $relationColumn + * @return string */ - public function ordering() + protected function joinEagerLoadedColumn($relation, $relationColumn) { - foreach ($this->request->orderableColumns() as $orderable) { - $column = $this->setupOrderColumn($orderable); - if (isset($this->columnDef['order'][$column])) { - $method = $this->columnDef['order'][$column]['method']; - $parameters = $this->columnDef['order'][$column]['parameters']; - $this->compileColumnQuery( - $this->getQueryBuilder(), $method, $parameters, $column, $orderable['direction'] - ); + $joins = []; + foreach ((array) $this->getQueryBuilder()->joins as $key => $join) { + $joins[] = $join->table; + } + + /** + * Add support nested relations. + */ + $lastQuery = $this->query; + + foreach (explode('.', $relation) as $eachRelation) { + $model = $lastQuery->getRelation($eachRelation); + + if ($model instanceof BelongsToMany) { + $pivot = $model->getTable(); + $pivotPK = $model->getForeignKey(); + $pivotFK = $model->getQualifiedParentKeyName(); + if (! in_array($pivot, $joins)) { + $this->getQueryBuilder()->leftJoin($pivot, $pivotPK, '=', $pivotFK); + } + $related = $model->getRelated(); + $table = $related->getTable(); + $tablePK = $related->getForeignKey(); + $tableFK = $related->getQualifiedKeyName(); + if (! in_array($table, $joins)) { + $this->getQueryBuilder()->leftJoin($table, $pivot . '.' . $tablePK, '=', $tableFK); + } } else { - /** - * If we perform a select("*"), the ORDER BY clause will look like this: - * ORDER BY * ASC - * which causes a query exception - * The temporary fix is modify `*` column to `id` column - */ - if ($column === '*') { - $column = 'id'; + $table = $model->getRelated()->getTable(); + if ($model instanceof HasOneOrMany) { + $foreign = $model->getForeignKey(); + $other = $model->getQualifiedParentKeyName(); + } else { + $foreign = $model->getQualifiedForeignKey(); + $other = $model->getQualifiedOtherKeyName(); + } + if (! in_array($table, $joins)) { + $this->getQueryBuilder()->leftJoin($table, $foreign, '=', $other); } - $this->getQueryBuilder()->orderBy($column, $orderable['direction']); } + + $lastQuery = $model->getQuery(); + } + + $column = $table . '.' . $relationColumn; + + return $column; + } + + /** + * Compile queries for column search. + * + * @param int $i + * @param mixed $column + * @param string $keyword + */ + protected function compileColumnSearch($i, $column, $keyword) + { + if ($this->request->isRegex($i)) { + $column = strstr($column, '(') ? $this->connection->raw($column) : $column; + $this->regexColumnSearch($column, $keyword); + } else { + $this->compileQuerySearch($this->query, $column, $keyword, ''); } } /** - * Get order by column name. + * Compile regex query column search. * - * @param array $orderable - * @return string + * @param mixed $column + * @param string $keyword + */ + protected function regexColumnSearch($column, $keyword) + { + if ($this->isOracleSql()) { + $sql = ! $this->isCaseInsensitive() ? 'REGEXP_LIKE( ' . $column . ' , ? )' : 'REGEXP_LIKE( LOWER(' . $column . ') , ?, \'i\' )'; + $this->query->whereRaw($sql, [$keyword]); + } elseif ($this->database == 'pgsql') { + $sql = ! $this->isCaseInsensitive() ? $column . ' ~ ?' : $column . ' ~* ? '; + $this->query->whereRaw($sql, [$keyword]); + } else { + $sql = ! $this->isCaseInsensitive() ? $column . ' REGEXP ?' : 'LOWER(' . $column . ') REGEXP ?'; + $this->query->whereRaw($sql, [Str::lower($keyword)]); + } + } + + /** + * Perform sorting of columns. + * + * @return void */ - private function setupOrderColumn(array $orderable) + public function ordering() { - $r_column = $this->request->input('columns')[$orderable['column']]; - $column = isset($r_column['name']) ? $r_column['name'] : $r_column['data']; - if ($column >= 0) { - $column = $this->setupColumnName($orderable['column'], true); + if ($this->orderCallback) { + call_user_func($this->orderCallback, $this->getQueryBuilder()); - return $column; + return; } - return $column; + foreach ($this->request->orderableColumns() as $orderable) { + $column = $this->getColumnName($orderable['column'], true); + + if ($this->isBlacklisted($column)) { + continue; + } + + if (isset($this->columnDef['order'][$column])) { + $method = $this->columnDef['order'][$column]['method']; + $parameters = $this->columnDef['order'][$column]['parameters']; + $this->compileColumnQuery( + $this->getQueryBuilder(), + $method, + $parameters, + $column, + $orderable['direction'] + ); + } else { + $valid = 1; + if (count(explode('.', $column)) > 1) { + $eagerLoads = $this->getEagerLoads(); + $parts = explode('.', $column); + $relationColumn = array_pop($parts); + $relation = implode('.', $parts); + + if (in_array($relation, $eagerLoads)) { + // Loop for nested relations + // This code is check morph many or not. + // If one of nested relation is MorphToMany + // we will call joinEagerLoadedColumn. + $lastQuery = $this->query; + $isMorphToMany = false; + foreach (explode('.', $relation) as $eachRelation) { + $relationship = $lastQuery->getRelation($eachRelation); + if (! ($relationship instanceof MorphToMany)) { + $isMorphToMany = true; + } + $lastQuery = $relationship; + } + if ($isMorphToMany) { + $column = $this->joinEagerLoadedColumn($relation, $relationColumn); + } else { + $valid = 0; + } + } + } + + if ($valid == 1) { + $this->getQueryBuilder()->orderBy($column, $orderable['direction']); + } + } + } } /** - * @inheritdoc + * Perform pagination + * + * @return void */ public function paging() { - $this->query->skip($this->request['start']) - ->take((int) $this->request['length'] > 0 ? $this->request['length'] : 10); + $this->query->skip($this->request->input('start')) + ->take((int) $this->request->input('length') > 0 ? $this->request->input('length') : 10); } /** diff --git a/src/Facades/Datatables.php b/src/Facades/Datatables.php new file mode 100644 index 00000000..695cb3f7 --- /dev/null +++ b/src/Facades/Datatables.php @@ -0,0 +1,24 @@ + + */ +class Datatables extends Facade +{ + /** + * Get the registered name of the component. + * + * @return string + */ + protected static function getFacadeAccessor() + { + return 'datatables'; + } +} diff --git a/src/Generators/DataTablesMakeCommand.php b/src/Generators/DataTablesMakeCommand.php index ece2fda5..49566815 100644 --- a/src/Generators/DataTablesMakeCommand.php +++ b/src/Generators/DataTablesMakeCommand.php @@ -3,7 +3,15 @@ namespace Yajra\Datatables\Generators; use Illuminate\Console\GeneratorCommand; +use Illuminate\Support\Str; +use Symfony\Component\Console\Input\InputOption; +/** + * Class DataTablesMakeCommand. + * + * @package Yajra\Datatables\Generators + * @author Arjay Angeles + */ class DataTablesMakeCommand extends GeneratorCommand { /** @@ -18,7 +26,7 @@ class DataTablesMakeCommand extends GeneratorCommand * * @var string */ - protected $description = 'Create a new DataTable Service class.'; + protected $description = 'Create a new DataTable service class.'; /** * The type of class being generated. @@ -28,14 +36,31 @@ class DataTablesMakeCommand extends GeneratorCommand protected $type = 'DataTable'; /** - * Get the default namespace for the class. + * The model class to be used by dataTable. * - * @param string $rootNamespace + * @var string + */ + protected $model; + + /** + * DataTable export filename. + * + * @var string + */ + protected $filename; + + /** + * Build the class with the given name. + * + * @param string $name * @return string */ - protected function getDefaultNamespace($rootNamespace) + protected function buildClass($name) { - return $rootNamespace . '\DataTables'; + $stub = $this->files->get($this->getStub()); + $stub = $this->replaceNamespace($stub, $name)->replaceClass($stub, $name); + + return $this->replaceModelImport($stub)->replaceModel($stub)->replaceFilename($stub); } /** @@ -47,4 +72,137 @@ protected function getStub() { return __DIR__ . '/stubs/datatables.stub'; } + + /** + * Replace model name. + * + * @param string $stub + * @return mixed + */ + protected function replaceModel(&$stub) + { + $model = explode('\\', $this->model); + $model = array_pop($model); + $stub = str_replace('ModelName', $model, $stub); + + return $this; + } + + /** + * Replace model import. + * + * @param string $stub + * @return $this + */ + protected function replaceModelImport(&$stub) + { + $stub = str_replace( + 'DummyModel', str_replace('\\\\', '\\', $this->model), $stub + ); + + return $this; + } + + /** + * Replace the filename. + * + * @param string $stub + * @return string + */ + protected function replaceFilename(&$stub) + { + $stub = str_replace( + 'DummyFilename', $this->filename, $stub + ); + + return $stub; + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions() + { + return [ + ['model', null, InputOption::VALUE_NONE, 'Use the provided name as the model.', null], + ]; + } + + /** + * Determine if the class already exists. + * + * @param string $rawName + * @return bool + */ + protected function alreadyExists($rawName) + { + $name = $this->parseName($rawName); + + $this->setModel($rawName); + $this->setFilename($rawName); + + return $this->files->exists($this->getPath($name)); + } + + /** + * Parse the name and format according to the root namespace. + * + * @param string $name + * @return string + */ + protected function parseName($name) + { + $rootNamespace = $this->laravel->getNamespace(); + + if (Str::startsWith($name, $rootNamespace)) { + return $name; + } + + if (Str::contains($name, '/')) { + $name = str_replace('/', '\\', $name); + } + + if (! Str::contains(Str::lower($name), 'datatable')) { + $name .= 'DataTable'; + } + + return $this->parseName($this->getDefaultNamespace(trim($rootNamespace, '\\')) . '\\' . $name); + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace . "\\" . $this->laravel['config']->get('datatables.namespace.base', 'DataTables'); + } + + /** + * Set the model to be used. + * + * @param string $name + */ + protected function setModel($name) + { + $rootNamespace = $this->laravel->getNamespace(); + $modelNamespace = $this->laravel['config']->get('datatables.namespace.model'); + $this->model = $this->option('model') + ? $rootNamespace . "\\" . ($modelNamespace ? $modelNamespace . "\\" : "") . $name + : $rootNamespace . "\\User"; + } + + /** + * Set the filename for export. + * + * @param string $name + */ + protected function setFilename($name) + { + $this->filename = Str::lower(Str::plural($name)); + } } diff --git a/src/Generators/DataTablesScopeCommand.php b/src/Generators/DataTablesScopeCommand.php index 478f6913..537c4575 100644 --- a/src/Generators/DataTablesScopeCommand.php +++ b/src/Generators/DataTablesScopeCommand.php @@ -4,6 +4,12 @@ use Illuminate\Console\GeneratorCommand; +/** + * Class DataTablesScopeCommand. + * + * @package Yajra\Datatables\Generators + * @author Arjay Angeles + */ class DataTablesScopeCommand extends GeneratorCommand { /** diff --git a/src/Generators/stubs/datatables.stub b/src/Generators/stubs/datatables.stub index 374d3961..250497a8 100644 --- a/src/Generators/stubs/datatables.stub +++ b/src/Generators/stubs/datatables.stub @@ -2,15 +2,11 @@ namespace DummyNamespace; -use App\User; +use DummyModel; use Yajra\Datatables\Services\DataTable; class DummyClass extends DataTable { - // protected $printPreview = 'path.to.print.preview.view'; - // protected $exportColumns = ['id', 'name']; - // protected $printColumns = '*'; - /** * Display ajax response. * @@ -25,15 +21,15 @@ class DummyClass extends DataTable } /** - * Get the query object to be processed by datatables. + * Get the query object to be processed by dataTables. * - * @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder + * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder|\Illuminate\Support\Collection */ public function query() { - $users = User::query(); + $query = ModelName::query(); - return $this->applyScopes($users); + return $this->applyScopes($query); } /** @@ -44,30 +40,34 @@ class DummyClass extends DataTable public function html() { return $this->builder() - ->columns([ - 'id', - // add columns to display - 'created_at', - 'updated_at', - ]) - ->addAction(['width' => '50px']) - ->ajax('') - ->parameters([ - 'buttons' => [ - 'create', - [ - 'extend' => 'collection', - 'text' => ' Export', - 'buttons' => [ - 'csv', - 'excel', - 'pdf', - ], - ], - 'print', - 'reset', - 'reload', - ], - ]); + ->columns($this->getColumns()) + ->ajax('') + ->addAction(['width' => '80px']) + ->parameters($this->getBuilderParameters()); + } + + /** + * Get columns. + * + * @return array + */ + protected function getColumns() + { + return [ + 'id', + // add your columns + 'created_at', + 'updated_at', + ]; + } + + /** + * Get filename for export. + * + * @return string + */ + protected function filename() + { + return 'DummyFilename_' . time(); } } diff --git a/src/Helper.php b/src/Helper.php index 7f37303f..43133814 100644 --- a/src/Helper.php +++ b/src/Helper.php @@ -8,18 +8,24 @@ use Illuminate\Support\Str; use Illuminate\View\Compilers\BladeCompiler; +/** + * Class Helper. + * + * @package Yajra\Datatables + * @author Arjay Angeles + */ class Helper { /** * Places item of extra columns into results by care of their order. * - * @param $item - * @param $array + * @param array $item + * @param array $array * @return array */ public static function includeInArray($item, $array) { - if ($item['order'] === false) { + if (self::isItemOrderInvalid($item, $array)) { return array_merge($array, [$item['name'] => $item['content']]); } else { $count = 0; @@ -38,6 +44,18 @@ 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) + { + return $item['order'] === false || $item['order'] >= count($array); + } + /** * Determines if content is callable or blade string, processes and returns. * @@ -91,6 +109,8 @@ public static function compileBlade($str, $data = []) } /** + * Get a mixed value of custom data and the parameters. + * * @param array $data * @param mixed $param * @return array @@ -109,7 +129,9 @@ public static function getMixedValue(array $data, $param) } /** - * @param $param + * Cast the parameter into an array. + * + * @param mixed $param * @return array */ public static function castToArray($param) @@ -120,6 +142,10 @@ public static function castToArray($param) return $param; } + if ($param instanceof Arrayable) { + return $param->toArray(); + } + return $param; } @@ -157,7 +183,7 @@ public static function wrapDatabaseValue($database, $value) } /** - * Database column wrapper + * Database column wrapper. * * @param string $database * @param string $key @@ -196,10 +222,12 @@ public static function wrapDatabaseColumn($database, $key, $column) public static function convertToArray($row) { $data = $row instanceof Arrayable ? $row->toArray() : (array) $row; - foreach (array_keys($data) as $key) { - if (is_object($data[$key]) || is_array($data[$key])) { - $data[$key] = self::convertToArray($data[$key]); + + foreach ($data as &$value) { + if (is_object($value) || is_array($value)) { + $value = self::convertToArray($value); } + unset($value); } return $data; @@ -217,8 +245,10 @@ public static function transform(array $data) } /** - * @param $row - * @return mixed + * Transform row data into an array. + * + * @param mixed $row + * @return array */ protected static function transformRow($row) { @@ -226,7 +256,7 @@ protected static function transformRow($row) if ($value instanceof DateTime) { $row[$key] = $value->format('Y-m-d H:i:s'); } else { - if (is_string($value)) { + if (is_object($value)) { $row[$key] = (string) $value; } else { $row[$key] = $value; diff --git a/src/Html/Builder.php b/src/Html/Builder.php index cb300268..cc4833cf 100644 --- a/src/Html/Builder.php +++ b/src/Html/Builder.php @@ -8,12 +8,14 @@ use Illuminate\Contracts\View\Factory; use Illuminate\Routing\UrlGenerator; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Str; /** - * Class Builder + * Class Builder. * * @package Yajra\Datatables\Html + * @author Arjay Angeles */ class Builder { @@ -67,6 +69,29 @@ class Builder */ protected $attributes = []; + /** + * Lists of valid DataTables Callbacks. + * + * @link https://datatables.net/reference/option/. + * @var array + */ + protected $validCallbacks = [ + 'createdRow', + 'drawCallback', + 'footerCallback', + 'formatNumber', + 'headerCallback', + 'infoCallback', + 'initComplete', + 'preDrawCallback', + 'rowCallback', + 'stateLoadCallback', + 'stateLoaded', + 'stateLoadParams', + 'stateSaveCallback', + 'stateSaveParams', + ]; + /** * @param Repository $config * @param Factory $view @@ -126,32 +151,206 @@ public function generateScripts() } /** - * Generate datatable js parameters. + * Generate DataTables js parameters. * * @param array $attributes * @return string */ public function parameterize($attributes = []) { - $parameters = (new Parameters($attributes))->toArray(); - $column_functions = []; + $parameters = (new Parameters($attributes))->toArray(); + + $values = []; + $replacements = []; + foreach($parameters as $key => &$value){ + if (!is_array($value)) { + if (strpos($value, '$.') === 0) + { + // Store function string. + $values[] = $value; + // Replace function string in $foo with a 'unique' special key. + $value = '%' . $key . '%'; + // Later on, we'll look for the value, and replace it. + $replacements[] = '"' . $value . '"'; + } + } + } + + list($ajaxDataFunction, $parameters) = $this->encodeAjaxDataFunction($parameters); + list($columnFunctions, $parameters) = $this->encodeColumnFunctions($parameters); + list($callbackFunctions, $parameters) = $this->encodeCallbackFunctions($parameters); + list($editorButtons, $parameters) = $this->encodeEditorButtons($parameters); + + $json = json_encode($parameters); + + $json = str_replace($replacements, $values, $json); + $json = $this->decodeAjaxDataFunction($ajaxDataFunction, $json); + $json = $this->decodeColumnFunctions($columnFunctions, $json); + $json = $this->decodeCallbackFunctions($callbackFunctions, $json); + $json = $this->decodeEditorButtons($editorButtons, $json); + + return $json; + } + + /** + * Encode ajax data function param. + * + * @param array $parameters + * @return mixed + */ + protected function encodeAjaxDataFunction($parameters) + { + $ajaxData = ''; + if (isset($parameters['ajax']['data'])) { + $ajaxData = $parameters['ajax']['data']; + $parameters['ajax']['data'] = "#ajax_data#"; + } + + return [$ajaxData, $parameters]; + } + + /** + * Encode columns render function. + * + * @param array $parameters + * @return array + */ + protected function encodeColumnFunctions(array $parameters) + { + $columnFunctions = []; foreach ($parameters['columns'] as $i => $column) { + unset($parameters['columns'][$i]['exportable']); + unset($parameters['columns'][$i]['printable']); + unset($parameters['columns'][$i]['footer']); + if (isset($column['render'])) { - $column_functions[$i] = $column['render']; + $columnFunctions[$i] = $column['render']; $parameters['columns'][$i]['render'] = "#column_function.{$i}#"; } } - $json = json_encode($parameters); + return [$columnFunctions, $parameters]; + } + + /** + * Encode DataTables callbacks function. + * + * @param array $parameters + * @return array + */ + protected function encodeCallbackFunctions(array $parameters) + { + $callbackFunctions = []; + foreach ($parameters as $key => $callback) { + if (in_array($key, $this->validCallbacks)) { + $callbackFunctions[$key] = $this->compileCallback($callback); + $parameters[$key] = "#callback_function.{$key}#"; + } + } + + return [$callbackFunctions, $parameters]; + } + + /** + * Encode DataTables editor buttons. + * + * @param array $parameters + * @return array + */ + protected function encodeEditorButtons(array $parameters) + { + $editorButtons = []; + if (isset($parameters['buttons'])) { + foreach ($parameters['buttons'] as $i => $button) { + if (isset($button['editor'])) { + $editorButtons[$i] = $this->compileCallback($button['editor']); + $parameters['buttons'][$i]['editor'] = "#editor_button.{$i}#"; + } + } + } + + + return [$editorButtons, $parameters]; + } - foreach ($column_functions as $i => $function) { + /** + * Compile DataTable callback value. + * + * @param mixed $callback + * @return mixed|string + */ + private function compileCallback($callback) + { + if (is_callable($callback)) { + return value($callback); + } elseif ($this->view->exists($callback)) { + return $this->view->make($callback)->render(); + } + + return $callback; + } + + /** + * Decode ajax data method. + * + * @param string $function + * @param string $json + * @return string + */ + protected function decodeAjaxDataFunction($function, $json) + { + return str_replace("\"#ajax_data#\"", $function, $json); + } + + /** + * Decode columns render functions. + * + * @param array $columnFunctions + * @param string $json + * @return string + */ + protected function decodeColumnFunctions(array $columnFunctions, $json) + { + foreach ($columnFunctions as $i => $function) { $json = str_replace("\"#column_function.{$i}#\"", $function, $json); } return $json; } + /** + * Decode DataTables callbacks function. + * + * @param array $callbackFunctions + * @param string $json + * @return string + */ + protected function decodeCallbackFunctions(array $callbackFunctions, $json) + { + foreach ($callbackFunctions as $i => $function) { + $json = str_replace("\"#callback_function.{$i}#\"", $function, $json); + } + + return $json; + } + + /** + * Decode DataTables Editor buttons. + * + * @param array $editorButtons + * @param string $json + * @return string + */ + protected function decodeEditorButtons(array $editorButtons, $json) + { + foreach ($editorButtons as $i => $function) { + $json = str_replace("\"#editor_button.{$i}#\"", $function, $json); + } + + return $json; + } + /** * Get javascript template to use. * @@ -164,6 +363,55 @@ protected function template() )->render(); } + /** + * Sets HTML table attribute(s). + * + * @param string|array $attribute + * @param mixed $value + * @return $this + */ + public function setTableAttribute($attribute, $value = null) + { + if (is_array($attribute)) { + $this->setTableAttributes($attribute); + } else { + $this->tableAttributes[$attribute] = $value; + } + + return $this; + } + + /** + * Sets multiple HTML table attributes at once. + * + * @param array $attributes + * @return $this + */ + public function setTableAttributes(array $attributes) + { + foreach ($attributes as $attribute => $value) { + $this->setTableAttribute($attribute, $value); + } + + return $this; + } + + /** + * Retrieves HTML table attribute value. + * + * @param string $attribute + * @return mixed + * @throws \Exception + */ + public function getTableAttribute($attribute) + { + if (! array_key_exists($attribute, $this->tableAttributes)) { + throw new \Exception("Table attribute '{$attribute}' does not exist."); + } + + return $this->tableAttributes[$attribute]; + } + /** * Add a column in collection using attributes. * @@ -199,17 +447,21 @@ public function add(Column $column) public function columns(array $columns) { foreach ($columns as $key => $value) { - if (is_array($value)) { - $attributes = array_merge(['name' => $key, 'data' => $key], $this->setTitle($key, $value)); + if (! is_a($value, Column::class)) { + if (is_array($value)) { + $attributes = array_merge(['name' => $key, 'data' => $key], $this->setTitle($key, $value)); + } else { + $attributes = [ + 'name' => $value, + 'data' => $value, + 'title' => $this->getQualifiedTitle($value), + ]; + } + + $this->collection->push(new Column($attributes)); } else { - $attributes = [ - 'name' => $value, - 'data' => $value, - 'title' => $this->getQualifiedTitle($value), - ]; + $this->collection->push($value); } - - $this->collection->push(new Column($attributes)); } return $this; @@ -250,17 +502,17 @@ public function getQualifiedTitle($title) */ public function addCheckbox(array $attributes = []) { - $attributes = array_merge( - [ - 'defaultContent' => 'html->attributes($attributes) . '/>', - 'title' => $this->form->checkbox('', '', false, ['id' => 'dataTablesCheckbox']), - 'data' => 'checkbox', - 'name' => 'checkbox', - 'orderable' => false, - 'searchable' => false, - 'width' => '10px', - ], $attributes - ); + $attributes = array_merge([ + 'defaultContent' => 'html->attributes($attributes) . '/>', + 'title' => $this->form->checkbox('', '', false, ['id' => 'dataTablesCheckbox']), + 'data' => 'checkbox', + 'name' => 'checkbox', + 'orderable' => false, + 'searchable' => false, + 'exportable' => false, + 'printable' => true, + 'width' => '10px', + ], $attributes); $this->collection->push(new Column($attributes)); return $this; @@ -274,22 +526,63 @@ public function addCheckbox(array $attributes = []) */ public function addAction(array $attributes = []) { - $attributes = array_merge( - [ - 'defaultContent' => '', - 'data' => 'action', - 'name' => 'action', - 'title' => 'Action', - 'render' => null, - 'orderable' => false, - 'searchable' => false, - ], $attributes - ); + $attributes = array_merge([ + 'defaultContent' => '', + 'data' => 'action', + 'name' => 'action', + 'title' => 'Action', + 'render' => null, + 'orderable' => false, + 'searchable' => false, + 'exportable' => false, + 'printable' => true, + 'footer' => '', + ], $attributes); + $this->collection->push(new Column($attributes)); + + return $this; + } + + /** + * Add a index column. + * + * @param array $attributes + * @return $this + */ + public function addIndex(array $attributes = []) + { + $indexColumn = Config::get('datatables.index_column', 'DT_Row_Index'); + $attributes = array_merge([ + 'defaultContent' => '', + 'data' => $indexColumn, + 'name' => $indexColumn, + 'title' => '', + 'render' => null, + 'orderable' => false, + 'searchable' => false, + 'exportable' => false, + 'printable' => true, + 'footer' => '', + ], $attributes); $this->collection->push(new Column($attributes)); return $this; } + /** + * Setup ajax parameter for datatables pipeline plugin. + * + * @param string $url + * @param string $pages + * @return $this + */ + public function pipeline($url, $pages) + { + $this->ajax = "$.fn.dataTable.pipeline({ url: '{$url}', pages: {$pages} })"; + + return $this; + } + /** * Setup ajax parameter * @@ -306,14 +599,59 @@ public function ajax($attributes) /** * Generate DataTable's table html. * - * @param array $attributes + * @param array $attributes + * @param bool $drawFooter * @return string */ - public function table(array $attributes = []) + public function table(array $attributes = [], $drawFooter = false) { $this->tableAttributes = array_merge($this->tableAttributes, $attributes); - return 'html->attributes($this->tableAttributes) . '>
'; + $th = $this->compileTableHeaders(); + $htmlAttr = $this->html->attributes($this->tableAttributes); + + $tableHtml = ''; + $tableHtml .= '' . implode('', $th) . ''; + if ($drawFooter) { + $tf = $this->compileTableFooter(); + $tableHtml .= '' . implode('', $tf) . ''; + } + $tableHtml .= '
'; + + return $tableHtml; + } + + /** + * Compile table headers and to support responsive extension. + * + * @return array + */ + private function compileTableHeaders() + { + $th = []; + foreach ($this->collection->toArray() as $row) { + $thAttr = $this->html->attributes( + array_only($row, ['class', 'id', 'width', 'style', 'data-class', 'data-hide']) + ); + $th[] = '' . $row['title'] . ''; + } + + return $th; + } + + /** + * Compile table footer contents. + * + * @return array + */ + private function compileTableFooter() + { + $footer = []; + foreach ($this->collection->toArray() as $row) { + $footer[] = '' . $row['footer'] . ''; + } + + return $footer; } /** @@ -341,4 +679,14 @@ public function setTemplate($template) return $this; } + + /** + * Get collection of columns. + * + * @return Collection + */ + public function getColumns() + { + return $this->collection; + } } diff --git a/src/Html/Column.php b/src/Html/Column.php index b39cc83c..fce85236 100644 --- a/src/Html/Column.php +++ b/src/Html/Column.php @@ -5,10 +5,11 @@ use Illuminate\Support\Fluent; /** - * Class Column + * Class Column. * * @package Yajra\Datatables\Html * @see https://datatables.net/reference/option/ for possible columns option + * @author Arjay Angeles */ class Column extends Fluent { @@ -19,6 +20,9 @@ public function __construct($attributes = []) { $attributes['orderable'] = isset($attributes['orderable']) ? $attributes['orderable'] : true; $attributes['searchable'] = isset($attributes['searchable']) ? $attributes['searchable'] : true; + $attributes['exportable'] = isset($attributes['exportable']) ? $attributes['exportable'] : true; + $attributes['printable'] = isset($attributes['printable']) ? $attributes['printable'] : true; + $attributes['footer'] = isset($attributes['footer']) ? $attributes['footer'] : ''; // Allow methods override attribute value foreach ($attributes as $attribute => $value) { @@ -34,17 +38,24 @@ public function __construct($attributes = []) /** * Parse render attribute. * - * @param \Closure|string $value + * @param mixed $value * @return string|null */ public function parseRender($value) { - $view = app('view'); + /** @var \Illuminate\Contracts\View\Factory $view */ + $view = app('view'); + $parameters = []; + + if (is_array($value)) { + $parameters = array_except($value, 0); + $value = $value[0]; + } if (is_callable($value)) { - return value($value); + return $value($parameters); } elseif ($view->exists($value)) { - return $view->make($value)->render(); + return $view->make($value)->with($parameters)->render(); } return $value ? $this->parseRenderAsString($value) : null; @@ -53,7 +64,7 @@ public function parseRender($value) /** * Display render value as is. * - * @param string $value + * @param mixed $value * @return string */ private function parseRenderAsString($value) diff --git a/src/Html/Parameters.php b/src/Html/Parameters.php index 38bfe21c..a462a44c 100644 --- a/src/Html/Parameters.php +++ b/src/Html/Parameters.php @@ -5,10 +5,11 @@ use Illuminate\Support\Fluent; /** - * Class Parameters + * Class Parameters. * * @package Yajra\Datatables\Html * @see https://datatables.net/reference/option/ for possible columns option + * @author Arjay Angeles */ class Parameters extends Fluent { diff --git a/src/Processors/DataProcessor.php b/src/Processors/DataProcessor.php index 7fff8e53..7c0861cf 100644 --- a/src/Processors/DataProcessor.php +++ b/src/Processors/DataProcessor.php @@ -2,67 +2,83 @@ namespace Yajra\Datatables\Processors; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Config; use Yajra\Datatables\Helper; /** - * Class DataProcessor + * Class DataProcessor. * * @package Yajra\Datatables + * @author Arjay Angeles */ class DataProcessor { + /** + * @var int + */ + protected $start; + /** * Columns to escape value. * * @var array */ - private $escapeColumns = []; + protected $escapeColumns = []; /** * Processed data output * * @var array */ - private $output = []; + protected $output = []; /** * @var array */ - private $appendColumns = []; + protected $appendColumns = []; /** * @var array */ - private $editColumns = []; + protected $editColumns = []; /** * @var array */ - private $excessColumns = []; + protected $excessColumns = []; /** * @var mixed */ - private $results; + protected $results; /** * @var array */ - private $templates; + protected $templates; + + /** + * @var bool + */ + protected $includeIndex; /** * @param mixed $results * @param array $columnDef * @param array $templates + * @param int $start */ - public function __construct($results, array $columnDef, array $templates) + public function __construct($results, array $columnDef, array $templates, $start) { $this->results = $results; $this->appendColumns = $columnDef['append']; $this->editColumns = $columnDef['edit']; $this->excessColumns = $columnDef['excess']; $this->escapeColumns = $columnDef['escape']; + $this->includeIndex = $columnDef['index']; $this->templates = $templates; + $this->start = $start; } /** @@ -74,6 +90,8 @@ public function __construct($results, array $columnDef, array $templates) public function process($object = false) { $this->output = []; + $indexColumn = Config::get('datatables.index_column', 'DT_Row_Index'); + foreach ($this->results as $row) { $data = Helper::convertToArray($row); $value = $this->addColumns($data, $row); @@ -81,6 +99,10 @@ public function process($object = false) $value = $this->setupRowVariables($value, $row); $value = $this->removeExcessColumns($value); + if ($this->includeIndex) { + $value[$indexColumn] = ++$this->start; + } + $this->output[] = $object ? $value : $this->flatten($value); } @@ -90,11 +112,11 @@ public function process($object = false) /** * Process add columns. * - * @param array $data + * @param mixed $data * @param mixed $row * @return array */ - protected function addColumns(array $data, $row) + protected function addColumns($data, $row) { foreach ($this->appendColumns as $key => $value) { $value['content'] = Helper::compileContent($value['content'], $data, $row); @@ -107,15 +129,15 @@ protected function addColumns(array $data, $row) /** * Process edit columns. * - * @param array $data + * @param mixed $data * @param mixed $row * @return array */ - protected function editColumns(array $data, $row) + protected function editColumns($data, $row) { foreach ($this->editColumns as $key => $value) { - $value['content'] = Helper::compileContent($value['content'], $data, $row); - $data[$value['name']] = $value['content']; + $value['content'] = Helper::compileContent($value['content'], $data, $row); + Arr::set($data, $value['name'], $value['content']); } return $data; diff --git a/src/Processors/RowProcessor.php b/src/Processors/RowProcessor.php index bdc229d3..3b55af77 100644 --- a/src/Processors/RowProcessor.php +++ b/src/Processors/RowProcessor.php @@ -6,9 +6,10 @@ use Yajra\Datatables\Helper; /** - * Class RowProcessor + * Class RowProcessor. * * @package Yajra\Datatables + * @author Arjay Angeles */ class RowProcessor { diff --git a/src/Request.php b/src/Request.php index 02754b94..3de2f376 100644 --- a/src/Request.php +++ b/src/Request.php @@ -6,7 +6,10 @@ use Illuminate\Http\Request as IlluminateRequest; /** - * @property array columns + * Class Request. + * + * @package Yajra\Datatables + * @author Arjay Angeles */ class Request extends IlluminateRequest { @@ -17,9 +20,9 @@ class Request extends IlluminateRequest */ public function checkLegacyCode() { - if (! $this->get('draw') && $this->get('sEcho')) { + if (! $this->input('draw') && $this->input('sEcho')) { throw new Exception('DataTables legacy code is not supported! Please use DataTables 1.10++ coding convention.'); - } elseif (! $this->get('draw') && ! $this->get('columns')) { + } elseif (! $this->input('draw') && ! $this->input('columns')) { throw new Exception('Insufficient parameters'); } } @@ -31,18 +34,7 @@ public function checkLegacyCode() */ public function isSearchable() { - return $this->get('search')['value'] != ''; - } - - /** - * Get column's search value. - * - * @param integer $index - * @return string - */ - public function columnKeyword($index) - { - return $this->columns[$index]['search']['value']; + return $this->input('search.value') != ''; } /** @@ -53,7 +45,7 @@ public function columnKeyword($index) */ public function isRegex($index) { - return $this->columns[$index]['search']['regex'] === 'true'; + return $this->input("columns.$index.search.regex") === 'true'; } /** @@ -68,9 +60,9 @@ public function orderableColumns() } $orderable = []; - for ($i = 0, $c = count($this->get('order')); $i < $c; $i++) { - $order_col = (int) $this->get('order')[$i]['column']; - $order_dir = $this->get('order')[$i]['dir']; + for ($i = 0, $c = count($this->input('order')); $i < $c; $i++) { + $order_col = (int) $this->input("order.$i.column"); + $order_dir = $this->input("order.$i.dir"); if ($this->isColumnOrderable($order_col)) { $orderable[] = ['column' => $order_col, 'direction' => $order_dir]; } @@ -86,7 +78,7 @@ public function orderableColumns() */ public function isOrderable() { - return $this->get('order') && count($this->get('order')) > 0; + return $this->input('order') && count($this->input('order')) > 0; } /** @@ -97,7 +89,7 @@ public function isOrderable() */ public function isColumnOrderable($index) { - return $this->get('columns')[$index]['orderable'] == 'true'; + return $this->input("columns.$index.orderable") == 'true'; } /** @@ -108,7 +100,7 @@ public function isColumnOrderable($index) public function searchableColumnIndex() { $searchable = []; - for ($i = 0, $c = count($this->get('columns')); $i < $c; $i++) { + for ($i = 0, $c = count($this->input('columns')); $i < $c; $i++) { if ($this->isColumnSearchable($i, false)) { $searchable[] = $i; } @@ -126,12 +118,22 @@ public function searchableColumnIndex() */ public function isColumnSearchable($i, $column_search = true) { - $columns = $this->get('columns'); if ($column_search) { - return $columns[$i]['searchable'] == 'true' && $columns[$i]['search']['value'] != ''; + return filter_var($this->input("columns.$i.searchable"), FILTER_VALIDATE_BOOLEAN) && $this->columnKeyword($i) != ''; } - return $columns[$i]['searchable'] == 'true'; + return filter_var($this->input("columns.$i.searchable"), FILTER_VALIDATE_BOOLEAN); + } + + /** + * Get column's search value. + * + * @param integer $index + * @return string + */ + public function columnKeyword($index) + { + return $this->input("columns.$index.search.value"); } /** @@ -141,7 +143,7 @@ public function isColumnSearchable($i, $column_search = true) */ public function keyword() { - return $this->get('search')['value']; + return $this->input('search.value'); } /** @@ -152,7 +154,9 @@ public function keyword() */ public function columnName($i) { - return $this->get('columns')[$i]['name']; + $column = $this->input("columns.$i"); + + return isset($column['name']) && $column['name'] <> '' ? $column['name'] : $column['data']; } /** @@ -162,6 +166,6 @@ public function columnName($i) */ public function isPaginationable() { - return ! is_null($this->get('start')) && ! is_null($this->get('length')) && $this->get('length') != -1; + return ! is_null($this->input('start')) && ! is_null($this->input('length')) && $this->input('length') != -1; } } diff --git a/src/Services/DataTable.php b/src/Services/DataTable.php index d1a0a66e..4fac8758 100644 --- a/src/Services/DataTable.php +++ b/src/Services/DataTable.php @@ -3,11 +3,23 @@ namespace Yajra\Datatables\Services; use Illuminate\Contracts\View\Factory; +use Illuminate\Support\Facades\Config; +use Maatwebsite\Excel\Classes\LaravelExcelWorksheet; +use Maatwebsite\Excel\Writers\LaravelExcelWriter; use Yajra\Datatables\Contracts\DataTableButtonsContract; use Yajra\Datatables\Contracts\DataTableContract; use Yajra\Datatables\Contracts\DataTableScopeContract; use Yajra\Datatables\Datatables; +use Yajra\Datatables\Transformers\DataTransformer; +use SimpleXMLElement; + +/** + * Class DataTable. + * + * @package Yajra\Datatables\Services + * @author Arjay Angeles + */ abstract class DataTable implements DataTableContract, DataTableButtonsContract { /** @@ -25,7 +37,7 @@ abstract class DataTable implements DataTableContract, DataTableButtonsContract * * @var string */ - protected $printPreview; + protected $printPreview = 'datatables::print'; /** * List of columns to be exported. @@ -44,11 +56,34 @@ abstract class DataTable implements DataTableContract, DataTableButtonsContract /** * Query scopes. * - * @var array + * @var \Yajra\Datatables\Contracts\DataTableScopeContract[] */ protected $scopes = []; /** + * Html builder. + * + * @var \Yajra\Datatables\Html\Builder + */ + protected $htmlBuilder; + + /** + * Export filename. + * + * @var string + */ + protected $filename = ''; + + /** + * Custom attributes set on the class. + * + * @var array + */ + protected $attributes = []; + + /** + * DataTable constructor. + * * @param \Yajra\Datatables\Datatables $datatables * @param \Illuminate\Contracts\View\Factory $viewFactory */ @@ -59,111 +94,126 @@ public function __construct(Datatables $datatables, Factory $viewFactory) } /** - * Render view. + * Process dataTables needed render output. * - * @param $view + * @param string $view * @param array $data * @param array $mergeData - * @return \Illuminate\Http\JsonResponse|\Illuminate\View\View + * @return \Illuminate\Contracts\View\View|\Illuminate\Http\JsonResponse|\Illuminate\View\View */ public function render($view, $data = [], $mergeData = []) { - if ($this->request()->ajax() && $this->request()->wantsJson()) { + if ($this->request()->ajax() && $this->request()->wantsJson()) { return $this->ajax(); } - switch ($this->datatables->getRequest()->get('action')) { - case 'excel': - return $this->excel(); - - case 'csv': - return $this->csv(); - - case 'pdf': - return $this->pdf(); - - case 'print': + if ($action = $this->request()->get('action') AND in_array($action, ['print', 'csv', 'excel', 'pdf'])) { + if ($action == 'print') { return $this->printPreview(); + } - default: - return $this->viewFactory->make($view, $data, $mergeData)->with('dataTable', $this->html()); + return call_user_func_array([$this, $action], []); } + + return $this->viewFactory->make($view, $data, $mergeData)->with('dataTable', $this->html()); } /** - * Export results to Excel file. + * Get Datatables Request instance. * - * @return mixed + * @return \Yajra\Datatables\Request */ - public function excel() + public function request() { - return $this->buildExcelFile()->download('xls'); + return $this->datatables->getRequest(); } /** - * Build excel file and prepare for export. + * Display printable view of datatables. * - * @return mixed + * @return \Illuminate\Contracts\View\View */ - protected function buildExcelFile() + public function printPreview() { - return app('excel')->create($this->filename(), function ($excel) { - $excel->sheet('exported-data', function ($sheet) { - $sheet->fromArray($this->getDataForExport()); - }); - }); + $data = $this->getDataForPrint(); + + return $this->viewFactory->make($this->printPreview, compact('data')); } /** - * Get filename for export. + * Get mapped columns versus final decorated output. * - * @return string + * @return array */ - protected function filename() + protected function getDataForPrint() { - return 'export_' . time(); + $columns = $this->printColumns(); + + return $this->mapResponseToColumns($columns, 'printable'); } /** - * Get mapped columns versus final decorated output. + * Get printable columns. * - * @return array + * @return array|string */ - protected function getDataForExport() + protected function printColumns() { - $decoratedData = $this->getAjaxResponseData(); + return is_array($this->printColumns) ? $this->printColumns : $this->getColumnsFromBuilder(); + } - return array_map(function ($row) { - if (is_array($this->exportColumns)) { - return array_only($row, $this->exportColumns); - } + /** + * Get columns definition from html builder. + * + * @return \Illuminate\Support\Collection + */ + protected function getColumnsFromBuilder() + { + return $this->html()->getColumns(); + } - return $row; - }, $decoratedData); + /** + * Optional method if you want to use html builder. + * + * @return \Yajra\Datatables\Html\Builder + */ + public function html() + { + return $this->builder(); } /** - * Get mapped columns versus final decorated output. + * Get Datatables Html Builder instance. * - * @return array + * @return \Yajra\Datatables\Html\Builder */ - protected function getDataForPrint() + public function builder() { - $decoratedData = $this->getAjaxResponseData(); + return $this->htmlBuilder ?: $this->htmlBuilder = $this->datatables->getHtmlBuilder(); + } - return array_map(function ($row) { - if (is_array($this->printColumns)) { - return array_only($row, $this->printColumns); + /** + * Map ajax response to columns definition. + * + * @param mixed $columns + * @param string $type + * @return array + */ + protected function mapResponseToColumns($columns, $type) + { + return array_map(function ($row) use ($columns, $type) { + if ($columns) { + return (new DataTransformer())->transform($row, $columns, $type); } return $row; - }, $decoratedData); + }, $this->getAjaxResponseData()); } /** * Get decorated data as defined in datatables ajax response. * - * @return mixed + * @return array */ protected function getAjaxResponseData() { @@ -176,66 +226,154 @@ protected function getAjaxResponseData() } /** - * Export results to CSV file. + * Export results to Excel file. * - * @return mixed + * @return void */ - public function csv() + public function excel() { - return $this->buildExcelFile()->download('csv'); + $this->buildExcelFile()->download('xls'); } /** - * Export results to PDF file. + * Build excel file and prepare for export. * - * @return mixed + * @return \Maatwebsite\Excel\Writers\LaravelExcelWriter */ - public function pdf() + protected function buildExcelFile() { - return $this->buildExcelFile()->download('pdf'); + /** @var \Maatwebsite\Excel\Excel $excel */ + $excel = app('excel'); + + return $excel->create($this->getFilename(), function (LaravelExcelWriter $excel) { + $excel->sheet('exported-data', function (LaravelExcelWorksheet $sheet) { + $sheet->fromArray($this->getDataForExport()); + $highestColumn = $sheet->getHighestColumn(); + + for($row = 1; $row <= sizeof($sheet->data); $row++) { + foreach (range('A', $highestColumn) as $column) { + $cell = $sheet->getCell($column.$row); + $cellValue = $cell->getValue(); + + if($cellValue != strip_tags($cellValue)) { + $tag = new SimpleXMLElement($cellValue); + + if($tag['href'] && filter_var($tag['href'], FILTER_VALIDATE_URL)) { + $value = $tag->__toString() ? $tag->__toString() : $tag['href']; + $cell->setValue($value) + ->getHyperlink() + ->setUrl($tag['href']); + } + } + } + } + }); + }); } /** - * Display printable view of datatables. + * Get export filename. * - * @return \Illuminate\Contracts\View\View + * @return string */ - public function printPreview() + public function getFilename() { - $data = $this->getDataForPrint(); - $view = $this->printPreview ?: 'datatables::print'; + return $this->filename ?: $this->filename(); + } - return $this->viewFactory->make($view, compact('data')); + /** + * Set export filename. + * + * @param string $filename + * @return DataTable + */ + public function setFilename($filename) + { + $this->filename = $filename; + + return $this; } /** - * Optional method if you want to use html builder. + * Get filename for export. * - * @return mixed + * @return string */ - public function html() + protected function filename() { - return $this->builder(); + return 'export_' . time(); } /** - * Get Datatables Html Builder instance. + * Get mapped columns versus final decorated output. * - * @return \Yajra\Datatables\Html\Builder + * @return array */ - public function builder() + protected function getDataForExport() { - return $this->datatables->getHtmlBuilder(); + $columns = $this->exportColumns(); + + return $this->mapResponseToColumns($columns, 'exportable'); } /** - * Get Datatables Request instance. + * Get export columns definition. * - * @return \Yajra\Datatables\Request + * @return array|string */ - public function request() + private function exportColumns() { - return $this->datatables->getRequest(); + return is_array($this->exportColumns) ? $this->exportColumns : $this->getColumnsFromBuilder(); + } + + /** + * Export results to CSV file. + * + * @return void + */ + public function csv() + { + $this->buildExcelFile()->download('csv'); + } + + /** + * Export results to PDF file. + * + * @return mixed + */ + public function pdf() + { + if ('snappy' == Config::get('datatables.pdf_generator', 'excel')) { + return $this->snappyPdf(); + } else { + $this->buildExcelFile()->download('pdf'); + } + } + + /** + * PDF version of the table using print preview blade template. + * + * @return mixed + */ + public function snappyPdf() + { + /** @var \Barryvdh\Snappy\PdfWrapper $snappy */ + $snappy = app('snappy.pdf.wrapper'); + + $options = Config::get('datatables.snappy.options', [ + 'no-outline' => true, + 'margin-left' => '0', + 'margin-right' => '0', + 'margin-top' => '10mm', + 'margin-bottom' => '10mm', + ]); + $orientation = Config::get('datatables.snappy.orientation', 'landscape'); + + $snappy->setOptions($options) + ->setOrientation($orientation); + + return $snappy->loadHTML($this->printPreview()) + ->download($this->getFilename() . ".pdf"); } /** @@ -251,13 +389,46 @@ public function addScope(DataTableScopeContract $scope) return $this; } + /** + * Set a custom class attribute. + * + * @param mixed $key + * @param mixed|null $value + * @return $this + */ + public function with($key, $value = null) + { + if (is_array($key)) { + $this->attributes = array_merge($this->attributes, $key); + } else { + $this->attributes[$key] = $value; + } + + return $this; + } + + /** + * Dynamically retrieve the value of an attribute. + * + * @param string $key + * @return mixed|null + */ + public function __get($key) + { + if (array_key_exists($key, $this->attributes)) { + return $this->attributes[$key]; + } + + return null; + } + /** * Apply query scopes. * * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query * @return mixed */ - public function applyScopes($query) + protected function applyScopes($query) { foreach ($this->scopes as $scope) { $scope->apply($query); @@ -265,4 +436,23 @@ public function applyScopes($query) return $query; } + + /** + * Get default builder parameters. + * + * @return array + */ + protected function getBuilderParameters() + { + return [ + 'order' => [[0, 'desc']], + 'buttons' => [ + 'create', + 'export', + 'print', + 'reset', + 'reload', + ], + ]; + } } diff --git a/src/Transformers/DataTransformer.php b/src/Transformers/DataTransformer.php new file mode 100644 index 00000000..67dddc62 --- /dev/null +++ b/src/Transformers/DataTransformer.php @@ -0,0 +1,80 @@ + + */ +class DataTransformer +{ + /** + * Transform row data by columns definition. + * + * @param array $row + * @param mixed $columns + * @param string $type + * @return array + */ + public function transform(array $row, $columns, $type = 'printable') + { + if ($columns instanceof Collection) { + return $this->buildColumnByCollection($row, $columns, $type); + } + + return array_only($row, $columns); + } + + /** + * Transform row column by collection. + * + * @param array $row + * @param \Illuminate\Support\Collection $columns + * @param string $type + * @return array + */ + protected function buildColumnByCollection(array $row, Collection $columns, $type = 'printable') + { + $results = []; + foreach ($columns->all() as $column) { + if ($column[$type]) { + $title = $column['title']; + $data = array_get($row, $column['data']); + if ($type == 'exportable') { + $data = $this->decodeContent($data); + $title = $this->decodeContent($title); + } + + $results[$title] = $data; + } + } + + return $results; + } + + /** + * Decode content to a readable text value. + * + * @param string $data + * @return string + */ + protected function decodeContent($data) + { + preg_match_all('/]+href=([\'"])(.+?)\1[^>]*>/i', $data, $result); + if ($result[2] && $result[2][0]) { + return $data; + } + + try { + $decoded = html_entity_decode(strip_tags($data), ENT_QUOTES, 'UTF-8'); + + return str_replace("\xc2\xa0", ' ', $decoded); + } catch (\Exception $e) { + return $data; + } + } +} diff --git a/src/config/config.php b/src/config/config.php index bce2fc82..7c657d8d 100644 --- a/src/config/config.php +++ b/src/config/config.php @@ -1,15 +1,106 @@ [ + /** + * Smart search will enclose search keyword with wildcard string "%keyword%". + * SQL: column LIKE "%keyword%" + */ + 'smart' => true, + + /** + * Case insensitive will search the keyword in lower case format. + * SQL: LOWER(column) LIKE LOWER(keyword) + */ 'case_insensitive' => true, + + /** + * Wild card will add "%" in between every characters of the keyword. + * SQL: column LIKE "%k%e%y%w%o%r%d%" + */ 'use_wildcards' => false, ], + /** + * DataTables fractal configurations. + */ 'fractal' => [ + /** + * Request key name to parse includes on fractal. + */ + 'includes' => 'include', + + /** + * Default fractal serializer. + */ 'serializer' => 'League\Fractal\Serializer\DataArraySerializer', ], + /** + * DataTables script view template. + */ 'script_template' => 'datatables::script', + + /** + * DataTables internal index id response column name. + */ + 'index_column' => 'DT_Row_Index', + + /** + * Namespaces used by the generator. + */ + 'namespace' => [ + /** + * Base namespace/directory to create the new file. + * This is appended on default Laravel namespace. + * Usage: php artisan datatables:make User + * Output: App\DataTables\UserDataTable + * With Model: App\User (default model) + * Export filename: users_timestamp + */ + 'base' => 'DataTables', + + /** + * Base namespace/directory where your model's are located. + * This is appended on default Laravel namespace. + * Usage: php artisan datatables:make Post --model + * Output: App\DataTables\PostDataTable + * With Model: App\Post + * Export filename: posts_timestamp + */ + 'model' => '', + ], + + /** + * PDF generator to be used when converting the table to pdf. + * Available generators: excel, snappy + * Snappy package: barryvdh/laravel-snappy + * Excel package: maatwebsite/excel + */ + 'pdf_generator' => 'excel', + + /** + * Snappy PDF options. + */ + 'snappy' => [ + 'options' => [ + 'no-outline' => true, + 'margin-left' => '0', + 'margin-right' => '0', + 'margin-top' => '10mm', + 'margin-bottom' => '10mm', + ], + 'orientation' => 'landscape', + ], + + /** + * JsonResponse header and options config. + */ + 'json' => [ + 'header' => [], + 'options' => 0, + ], ]; diff --git a/src/resources/assets/buttons.server-side.js b/src/resources/assets/buttons.server-side.js index 5665c0b5..50ffa174 100644 --- a/src/resources/assets/buttons.server-side.js +++ b/src/resources/assets/buttons.server-side.js @@ -22,6 +22,18 @@ } }; + DataTable.ext.buttons.export = { + extend: 'collection', + + className: 'buttons-export', + + text: function (dt) { + return ' ' + dt.i18n('buttons.export', 'Export') + ' '; + }, + + buttons: ['csv', 'excel', 'pdf'] + }; + DataTable.ext.buttons.csv = { className: 'buttons-csv', diff --git a/src/resources/views/print.blade.php b/src/resources/views/print.blade.php index a7565609..1960a7d3 100644 --- a/src/resources/views/print.blade.php +++ b/src/resources/views/print.blade.php @@ -6,26 +6,30 @@ - + - +
+ @php ($flag = true) @foreach($data as $row) - @if ($row == reset($data)) + @if ($row == reset($data) && $flag) @foreach($row as $key => $value) @endforeach + @php ($flag = false) @endif @foreach($row as $key => $value) - @if (is_string($value) || trim($value)==='' || is_numeric($value)) + @if(is_string($value) || is_numeric($value)) + @else + @endif @endforeach diff --git a/tests/CollectionEngineTest.php b/tests/CollectionEngineTest.php index 424cb0f3..4fdd54a0 100644 --- a/tests/CollectionEngineTest.php +++ b/tests/CollectionEngineTest.php @@ -17,7 +17,7 @@ public function setUp() $app->shouldReceive('instance')->once()->andReturn($app); Illuminate\Support\Facades\Facade::setFacadeApplication($app); - Config::swap($config = m::mock('ConfigMock')); + Config::swap(new \Illuminate\Config\Repository); } public function tearDown() @@ -59,7 +59,6 @@ public function test_datatables_make_with_data() protected function setupBuilder() { - Config::shouldReceive('get'); $data = [ ['id' => 1, 'name' => 'foo'], ['id' => 2, 'name' => 'bar'], diff --git a/tests/HelperTest.php b/tests/HelperTest.php index c7289445..6ab97bd5 100644 --- a/tests/HelperTest.php +++ b/tests/HelperTest.php @@ -44,6 +44,27 @@ public function test_include_in_array_with_order() $this->assertEquals($expected, $data); } + public function test_include_in_array_with_order_outside_of_array_length() + { + $data = [ + 'id' => 1, + 'foo' => 'bar', + ]; + $item = [ + 'name' => 'user', + 'content' => 'John', + 'order' => 2, + ]; + + $data = Helper::includeInArray($item, $data); + $expected = [ + 'id' => 1, + 'foo' => 'bar', + 'user' => 'John', + ]; + $this->assertEquals($expected, $data); + } + public function test_compile_content_blade() { $content = '{!! $id !!}'; diff --git a/tests/HtmlBuilderTest.php b/tests/HtmlBuilderTest.php index 50916b25..58dede9e 100644 --- a/tests/HtmlBuilderTest.php +++ b/tests/HtmlBuilderTest.php @@ -12,7 +12,7 @@ class HtmlBuilderTest extends PHPUnit_Framework_TestCase public function test_generate_table_html() { $builder = app(Builder::class); - $builder->html->shouldReceive('attributes')->times(2)->andReturn('id="foo"'); + $builder->html->shouldReceive('attributes')->times(8)->andReturn('id="foo"'); $builder->form->shouldReceive('checkbox') ->once() ->andReturn(''); @@ -25,7 +25,8 @@ public function test_generate_table_html() ->ajax('ajax-url') ->parameters(['bFilter' => false]); $table = $builder->table(['id' => 'foo']); - $this->assertEquals('
{!! $key !!}
{!! $value !!}
', $table); + $expected = '
FooBarIdAOptions
'; + $this->assertEquals($expected, $table); $builder->view->shouldReceive('make')->times(2)->andReturn($builder->view); $builder->config->shouldReceive('get')->times(2)->andReturn('datatables::script'); @@ -41,6 +42,100 @@ public function test_generate_table_html() $this->assertEquals($expected, $builder->generateScripts()); } + public function test_generate_table_html_with_empty_footer() + { + $builder = app(Builder::class); + $builder->html->shouldReceive('attributes')->times(8)->andReturn('id="foo"'); + $builder->form->shouldReceive('checkbox') + ->once() + ->andReturn(''); + + $builder->addCheckbox(['id' => 'foo']) + ->columns(['foo', 'bar' => ['data' => 'foo']]) + ->addColumn(['name' => 'id', 'data' => 'id', 'title' => 'Id']) + ->add(new Column(['name' => 'a', 'data' => 'a', 'title' => 'A'])) + ->addAction(['title' => 'Options']) + ->ajax('ajax-url') + ->parameters(['bFilter' => false]); + $table = $builder->table(['id' => 'foo'], true); + $expected = '
FooBarIdAOptions
'; + $this->assertEquals($expected, $table); + + $builder->view->shouldReceive('make')->times(2)->andReturn($builder->view); + $builder->config->shouldReceive('get')->times(2)->andReturn('datatables::script'); + $template = file_get_contents(__DIR__ . '/../src/resources/views/script.blade.php'); + $builder->view->shouldReceive('render')->times(2)->andReturn(trim($template)); + $builder->html->shouldReceive('attributes')->once()->andReturn(); + + $script = $builder->scripts(); + $expected = '' . PHP_EOL; + $this->assertEquals($expected, $script); + + $expected = '(function(window,$){window.LaravelDataTables=window.LaravelDataTables||{};window.LaravelDataTables["foo"]=$("#foo").DataTable({"serverSide":true,"processing":true,"ajax":"ajax-url","columns":[{"defaultContent":"","title":"","data":"checkbox","name":"checkbox","orderable":false,"searchable":false,"width":"10px","id":"foo"},{"name":"foo","data":"foo","title":"Foo","orderable":true,"searchable":true},{"name":"bar","data":"foo","title":"Bar","orderable":true,"searchable":true},{"name":"id","data":"id","title":"Id","orderable":true,"searchable":true},{"name":"a","data":"a","title":"A","orderable":true,"searchable":true},{"defaultContent":"","data":"action","name":"action","title":"Options","render":null,"orderable":false,"searchable":false}],"bFilter":false});})(window,jQuery);'; + $this->assertEquals($expected, $builder->generateScripts()); + } + + public function test_generate_table_html_with_footer_content() + { + $builder = app(Builder::class); + $builder->html->shouldReceive('attributes')->times(8)->andReturn('id="foo"'); + $builder->form->shouldReceive('checkbox') + ->once() + ->andReturn(''); + + $builder->addCheckbox(['id' => 'foo', 'footer' => 'test']) + ->columns(['foo', 'bar' => ['data' => 'foo']]) + ->addColumn(['name' => 'id', 'data' => 'id', 'title' => 'Id']) + ->add(new Column(['name' => 'a', 'data' => 'a', 'title' => 'A'])) + ->addAction(['title' => 'Options']) + ->ajax('ajax-url') + ->parameters(['bFilter' => false]); + $table = $builder->table(['id' => 'foo'], true); + $expected = '
FooBarIdAOptions
test
'; + $this->assertEquals($expected, $table); + + $builder->view->shouldReceive('make')->times(2)->andReturn($builder->view); + $builder->config->shouldReceive('get')->times(2)->andReturn('datatables::script'); + $template = file_get_contents(__DIR__ . '/../src/resources/views/script.blade.php'); + $builder->view->shouldReceive('render')->times(2)->andReturn(trim($template)); + $builder->html->shouldReceive('attributes')->once()->andReturn(); + + $script = $builder->scripts(); + $expected = '' . PHP_EOL; + $this->assertEquals($expected, $script); + + $expected = '(function(window,$){window.LaravelDataTables=window.LaravelDataTables||{};window.LaravelDataTables["foo"]=$("#foo").DataTable({"serverSide":true,"processing":true,"ajax":"ajax-url","columns":[{"defaultContent":"","title":"","data":"checkbox","name":"checkbox","orderable":false,"searchable":false,"width":"10px","id":"foo"},{"name":"foo","data":"foo","title":"Foo","orderable":true,"searchable":true},{"name":"bar","data":"foo","title":"Bar","orderable":true,"searchable":true},{"name":"id","data":"id","title":"Id","orderable":true,"searchable":true},{"name":"a","data":"a","title":"A","orderable":true,"searchable":true},{"defaultContent":"","data":"action","name":"action","title":"Options","render":null,"orderable":false,"searchable":false}],"bFilter":false});})(window,jQuery);'; + $this->assertEquals($expected, $builder->generateScripts()); + } + + public function test_setting_table_attribute() + { + $builder = app(Builder::class); + + $builder->setTableAttribute('attr', 'val'); + + $this->assertEquals('val', $builder->getTableAttribute('attr')); + } + + public function test_settings_multiple_table_attributes() + { + $builder = app(Builder::class); + + $builder->setTableAttribute(['prop1' => 'val1', 'prop2' => 'val2']); + + $this->assertEquals('val1', $builder->getTableAttribute('prop1')); + $this->assertEquals('val2', $builder->getTableAttribute('prop2')); + } + + public function test_getting_inexistent_table_attribute_throws() + { + $this->setExpectedExceptionRegExp(\Exception::class, '/Table attribute \'.+?\' does not exist\./'); + + $builder = app(Builder::class); + + $builder->getTableAttribute('boohoo'); + } + /** * @return \Yajra\Datatables\Datatables */ diff --git a/tests/QueryBuilderEngineTest.php b/tests/QueryBuilderEngineTest.php index 21c504fe..64349160 100644 --- a/tests/QueryBuilderEngineTest.php +++ b/tests/QueryBuilderEngineTest.php @@ -17,7 +17,7 @@ public function setUp() $app->shouldReceive('instance')->once()->andReturn($app); Illuminate\Support\Facades\Facade::setFacadeApplication($app); - Config::swap($config = m::mock('ConfigMock')); + Config::swap(new \Illuminate\Config\Repository); } public function tearDown() @@ -76,8 +76,6 @@ public function test_datatables_make_with_data_using_alias() protected function setupBuilder($showAllRecords = false) { - Config::shouldReceive('get'); - $cache = m::mock('stdClass'); $driver = m::mock('stdClass'); $data = [