├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── src ├── Controller │ └── Component │ │ └── DataTablesComponent.php └── View │ └── Helper │ └── DataTablesHelper.php └── webroot └── js └── cakephp.dataTables.js /.gitignore: -------------------------------------------------------------------------------- 1 | CakePHP 3 2 | 3 | /vendor/* 4 | /config/app.php 5 | /tmp/* 6 | /logs/* 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Frank Heider 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cakephp-datatables 2 | 3 | This plugin implements the jQuery dataTables plugin (www.datatables.net) in your CakePHP 3 application. 4 | In addition there was added a multiple column search with request delay to minimize the ajax requests. 5 | 6 | 7 | ## Requirements 8 | 9 | * CakePHP 3 (http://www.cakephp.org) 10 | * jQuery (http://www.jquery.com) 11 | * jQuery DataTables (http://www.datatables.net) 12 | * Composer (http://getcomposer.org) 13 | 14 | 15 | ## Optional 16 | 17 | * Twitter Bootstrap 3 (http://getbootstrap.com) 18 | * FontAwesome 4 (http://fortawesome.github.io/Font-Awesome) 19 | 20 | The core templates are written in Twitter Bootstrap syntax and included FontAwesome icons but can be changed easily. 21 | 22 | 23 | ## Usage 24 | 25 | ### Step 1: Installation 26 | 27 | Use composer to install this plugin. 28 | Add the following repository and requirement to your composer.json: 29 | 30 | "require": { 31 | "fheider/cakephp-datatables": "dev-master" 32 | } 33 | 34 | 35 | ### Step 2: Include CakePHP Plugin and load Component and Helper 36 | 37 | Load plugin in ***app/bootstrap.php***: 38 | 39 | Plugin::load('DataTables', ['bootstrap' => false, 'routes' => false]); 40 | 41 | 42 | 43 | 44 | Include component and helper: 45 | 46 | class AppController extends Controller 47 | { 48 | 49 | public $helpers = [ 50 | 'DataTables' => [ 51 | 'className' => 'DataTables.DataTables' 52 | ] 53 | ]; 54 | 55 | public function initialize() 56 | { 57 | $this->loadComponent('DataTables.DataTables'); 58 | } 59 | 60 | } 61 | 62 | ### Step 3: Include assets 63 | 64 | Include jQuery and jQuery DataTables scripts first and then the dataTables logic: 65 | 66 | echo $this->Html->script('*PATH*/jquery.min.js'); 67 | echo $this->Html->script('*PATH*/jquery.dataTables.min.js'); 68 | echo $this->Html->script('*PATH*/dataTables.bootstrap.min.js'); (Optional) 69 | echo $this->Html->script('DataTables.cakephp.dataTables.js'); 70 | 71 | Include dataTables css: 72 | 73 | echo $this->Html->css('PATH/dataTables.bootstrap.css'); 74 | 75 | 76 | ### Step 4: Add business logic in your controller 77 | 78 | Use it simply like find: 79 | 80 | $data = $this->DataTables->find('*TABLE*', [ 81 | 'contain' => [] 82 | ]); 83 | 84 | $this->set([ 85 | 'data' => $data, 86 | '_serialize' => array_merge($this->viewVars['_serialize'], ['data']) 87 | ]); 88 | 89 | The array_merge is required because the component add multiple vars to view like recordsTotal, recordsFiltered, ... 90 | So your serialized data were added to this vars. 91 | 92 | 93 | ### Step 5: Template / View 94 | 95 | First display your table normal, so no additional request were sended by dataTables. 96 | The table foot is used for the multiple search fields. This could be input- or select-elements. 97 | 98 | 99 | 100 | 101 | ... 102 | 103 | 104 | 105 | 106 | 107 | 108 | ... 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | ... 117 | 118 | 119 | 120 |
id ?>name ?>
121 | 122 | 123 | Then add the dataTables logic. 124 | The options are exaxt the options you get in the dataTables reference (https://datatables.net/reference/option/). 125 | 126 | $this->DataTables->init([ 127 | 'ajax' => [ 128 | 'url' => Router::url(['action' => 'index']), 129 | ], 130 | 'deferLoading' => $recordsTotal, 131 | 'delay' => 600, 132 | 'columns' => [ 133 | [ 134 | 'name' => '*MODEL*.id', 135 | 'data' => 'id' 136 | 'orderable' => false 137 | ], 138 | [ 139 | 'name' => '*MODEL*.name', 140 | 'data' => 'name' 141 | ], 142 | ... 143 | ] 144 | ])->draw('.dataTable'); 145 | 146 | 147 | In draw method you set the selector of your table. Delay is an additional option for setting the delay for processing 148 | your search input. If delay is 0 on every key press a request will be sent. 149 | 150 | **Notes to columns settings** 151 | 152 | Every column contains 2 important informations: 153 | 154 | name = name of your table and field like 'Customers.id' 155 | data = name of the field in json response 156 | 157 | The option **name** is needed for sorting and filtering. The option **data** is needed for processing the json response. 158 | You also can easily add related data (e.g. a customer belongs to a customer group) 159 | 160 | name = Group.name 161 | data = group.name 162 | 163 | **Please keep in mind!** 164 | It is important that the amount of your columns array is the same like your columns in your HTML-Table! 165 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fheider/cakephp-datatables", 3 | "description": "Use jQuery dataTables for CakePHP 3", 4 | "homepage": "https://github.com/fheider/cakephp-datatables", 5 | "type": "cakephp-plugin", 6 | "keywords": ["cakephp", "datatables"], 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Frank Heider", 11 | "homepage": "https://github.com/fheider", 12 | "role": "Author" 13 | } 14 | ], 15 | "support": { 16 | "issues": "https://github.com/fheider/cakephp-datatables/issues", 17 | "source": "https://github.com/fheider/cakephp-datatables" 18 | }, 19 | "require": { 20 | "php": ">=5.4.16", 21 | "cakephp/cakephp": "~3.0" 22 | }, 23 | "suggest": { 24 | "phpunit/phpunit": "Allows automated tests to be run without system-wide install.", 25 | "cakephp/cakephp-codesniffer": "Allows to check the code against the coding standards used in CakePHP." 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "DataTables\\": "src" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "DataTables\\Test\\": "tests", 35 | "Cake\\Test\\": "./vendor/cakephp/cakephp/tests" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Controller/Component/DataTablesComponent.php: -------------------------------------------------------------------------------- 1 | 0, 15 | 'length' => 10, 16 | 'order' => [], 17 | 'conditionsOr' => [], // table-wide search conditions 18 | 'conditionsAnd' => [], // column search conditions 19 | 'matching' => [], // column search conditions for foreign tables 20 | ]; 21 | 22 | protected $_viewVars = [ 23 | 'recordsTotal' => 0, 24 | 'recordsFiltered' => 0, 25 | 'draw' => 0 26 | ]; 27 | 28 | protected $_isAjaxRequest = false; 29 | 30 | protected $_tableName = null; 31 | 32 | protected $_plugin = null; 33 | 34 | /** 35 | * Process query data of ajax request 36 | * 37 | */ 38 | private function _processRequest() 39 | { 40 | // -- check whether it is an ajax call from data tables server-side plugin or a normal request 41 | $this->_isAjaxRequest = $this->request->is('ajax'); 42 | 43 | // -- add limit 44 | if( isset($this->request->query['length']) && !empty($this->request->query['length']) ) 45 | { 46 | $this->config('length', $this->request->query['length']); 47 | } 48 | 49 | // -- add offset 50 | if( isset($this->request->query['start']) && !empty($this->request->query['start']) ) 51 | { 52 | $this->config('start', (int)$this->request->query['start']); 53 | } 54 | 55 | // -- add order 56 | if( isset($this->request->query['order']) && !empty($this->request->query['order']) ) 57 | { 58 | $order = $this->config('order'); 59 | foreach($this->request->query['order'] as $item) { 60 | $order[$this->request->query['columns'][$item['column']]['name']] = $item['dir']; 61 | } 62 | $this->config('order', $order); 63 | } 64 | 65 | // -- add draw (an additional field of data tables plugin) 66 | if( isset($this->request->query['draw']) && !empty($this->request->query['draw']) ) 67 | { 68 | $this->_viewVars['draw'] = (int)$this->request->query['draw']; 69 | } 70 | 71 | // -- don't support any search if columns data missing 72 | if( !isset($this->request->query['columns']) || 73 | empty($this->request->query['columns']) ) 74 | { 75 | return; 76 | } 77 | 78 | // -- check table search field 79 | $globalSearch = (isset($this->request->query['search']['value']) ? 80 | $this->request->query['search']['value'] : false); 81 | 82 | // -- add conditions for both table-wide and column search fields 83 | foreach($this->request->query['columns'] as $column) 84 | { 85 | if( $globalSearch && $column['searchable'] == 'true' ) { 86 | $this->_addCondition( $column['name'], $globalSearch, 'or' ); 87 | } 88 | $localSearch = $column['search']['value']; 89 | /* In some circumstances (no "table-search" row present), DataTables 90 | fills in all column searches with the global search. Compromise: 91 | Ignore local field if it matches global search. */ 92 | if( !empty($localSearch) && ($localSearch !== $globalSearch) ) { 93 | $this->_addCondition( $column['name'], $column['search']['value'] ); 94 | } 95 | } 96 | 97 | } 98 | 99 | /** 100 | * Find data 101 | * 102 | * @param $tableName 103 | * @param $finder 104 | * @param array $options 105 | * @return array|\Cake\ORM\Query 106 | */ 107 | public function find($tableName, $finder = 'all', array $options = []) 108 | { 109 | 110 | // -- get table object 111 | $table = TableRegistry::get($tableName); 112 | $this->_tableName = $table->alias(); 113 | 114 | // -- get query options 115 | $this->_processRequest(); 116 | $data = $table->find($finder, $options); 117 | 118 | // -- record count 119 | $this->_viewVars['recordsTotal'] = $data->count(); 120 | 121 | // -- filter result 122 | $data->where($this->config('conditionsAnd')); 123 | foreach($this->config('matching') as $association => $where) { 124 | $data->matching( $association, function ($q) use ($where) { 125 | return $q->where($where); 126 | }); 127 | }; 128 | $data->andWhere(['or' => $this->config('conditionsOr')]); 129 | 130 | $this->_viewVars['recordsFiltered'] = $data->count(); 131 | 132 | // -- add limit 133 | $data->limit( $this->config('length') ); 134 | $data->offset( $this->config('start') ); 135 | 136 | // -- sort 137 | $data->order( $this->config('order') ); 138 | 139 | // -- set all view vars to view and serialize array 140 | $this->_setViewVars(); 141 | return $data; 142 | 143 | } 144 | 145 | private function _getController() 146 | { 147 | return $this->_registry->getController(); 148 | } 149 | 150 | private function _setViewVars() 151 | { 152 | $_serialize = []; 153 | foreach($this->_viewVars as $field => $value) { 154 | $_serialize[] = $field; 155 | } 156 | $this->_getController()->set($this->_viewVars); 157 | $this->_getController()->set('_serialize', $_serialize); 158 | } 159 | 160 | private function _addCondition($column, $value, $type = 'and') 161 | { 162 | $condition = ["$column LIKE" => "$value%"]; 163 | 164 | if( $type === 'or' ) { 165 | $this->config('conditionsOr', $condition); // merges 166 | return; 167 | } 168 | 169 | list($association, $field) = explode('.', $column); 170 | if( $this->_tableName == $association) { 171 | $this->config('conditionsAnd', $condition); // merges 172 | } else { 173 | $this->config('matching', [$association => $condition]); // merges 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/View/Helper/DataTablesHelper.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'dataSrc' => 'data' 22 | ], 23 | 'searching' => true, 24 | 'processing' => true, 25 | 'serverSide' => true, 26 | 'deferRender' => true, 27 | 'dom' => '<<"row"<"col-sm-4"i><"col-sm-8"lp>>rt>', 28 | 'delay' => 600 29 | ]; 30 | 31 | public function init(array $options = []) 32 | { 33 | $this->_templater = $this->templater(); 34 | $this->config($options); 35 | 36 | // -- load i18n 37 | $this->config('language', [ 38 | 'paginate' => [ 39 | 'next' => '', 40 | 'previous' => '' 41 | ], 42 | 'processing' => __d('DataTables', 'Your request is processing ...'), 43 | 'lengthMenu' => 44 | '', 50 | 'info' => __d('DataTables', 'Showing _START_ to _END_ of _TOTAL_ entries'), 51 | 'infoFiltered' => __d('DataTables', '(filtered from _MAX_ total entries)') 52 | ]); 53 | 54 | return $this; 55 | } 56 | 57 | public function draw($selector) 58 | { 59 | return sprintf('delay=%d;table=jQuery("%s").dataTable(%s);initSearch();', $this->config('delay'), $selector, json_encode($this->config()) ); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /webroot/js/cakephp.dataTables.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Table instance 3 | * 4 | */ 5 | var table = null; 6 | 7 | /** 8 | * Timer instance 9 | * 10 | */ 11 | var oFilterTimerId = null; 12 | 13 | /** 14 | * Default filter delay to optimize performance 15 | * @type {number} 16 | */ 17 | var delay = 600; 18 | 19 | /** 20 | * Add search behavior to all search fields in column footer 21 | */ 22 | function initSearch () 23 | { 24 | table.api().columns().every( function () { 25 | var index = this.index(); 26 | var lastValue = ''; // closure variable to prevent redundant AJAX calls 27 | $('input, select', this.footer()).on('keyup change', function () { 28 | if (this.value != lastValue) { 29 | lastValue = this.value; 30 | // -- set search 31 | table.api().column(index).search(this.value); 32 | window.clearTimeout(oFilterTimerId); 33 | oFilterTimerId = window.setTimeout(drawTable, delay); 34 | } 35 | }); 36 | }); 37 | } 38 | 39 | /** 40 | * Function reset 41 | * 42 | */ 43 | function reset() 44 | { 45 | table.api().columns().every(function() { 46 | this.search(''); 47 | $('input, select', this.footer()).val(''); 48 | drawTable(); 49 | }); 50 | } 51 | 52 | /** 53 | * Draw table again after changes 54 | * 55 | */ 56 | function drawTable() { 57 | table.api().draw(); 58 | } 59 | --------------------------------------------------------------------------------