├── .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 |
= $item->id ?>
115 |
= $item->name ?>
116 | ...
117 |
118 |
119 |
120 |
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 |
--------------------------------------------------------------------------------