├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── src ├── Controller │ └── Component │ │ └── DataTablesComponent.php ├── Lib │ ├── CallbackFunction.php │ ├── ColumnDefinition.php │ └── ColumnDefinitions.php ├── Locale │ └── de │ │ └── data_tables.po └── 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 | [DataTables](https://www.datatables.net) is a jQuery plugin for intelligent HTML tables. Next to adding dynamic elements to the table, it also has great supports for on-demand data fetching and server-side processing. The _cakephp-datatables_ plugin makes it easy to use the functionality DataTables provides in your CakePHP 3 application. It consists of a helper to add DataTables to your view and a Component to transparently process AJAX requests made by DataTables. 4 | 5 | ## Versioning 6 | 7 | * Versions 4.x are for users of CakePHP 4.0 and above 8 | * Versions 3.x are for users of CakePHP 3.6 and above 9 | * Versions 2.x are available for older CakePHP installations, but will not receive new features 10 | * Version 1.0 is a tag available for people who let their code rot. Consider upgrading by only changing a couple of lines! 11 | * Branch `php5` is for people without PHP 7 and currently stuck at version 1.0 12 | 13 | ## Requirements 14 | 15 | * PHP 7 or 8 16 | * CakePHP 5.x 17 | * DataTables 1.x or 2.x 18 | 19 | ## Installation and Usage 20 | 21 | Please see the [Documentation][doc], esp. the [Quick Start tutorial][quickstart] 22 | 23 | [doc]: https://github.com/ypnos-web/cakephp-datatables/wiki 24 | [quickstart]: https://github.com/ypnos-web/cakephp-datatables/wiki/Quick-Start 25 | 26 | 27 | ## Credits 28 | 29 | This work is based on the [code by Frank Heider](https://github.com/fheider/cakephp-datatables) and incorporates [code by Xavier Zolezzi](https://github.com/x-zolezzi/cakephp-datatables). 30 | 31 | ___ 32 | ## IMPORTANT SECURITY NOTICE for users prior to Oct 24, 2017 33 | 34 | The original code by fheider is vulnerable to SQL injection attacks, which was made apparent by a recent 35 | [addition to the CakePHP documentation](https://github.com/cakephp/cakephp/commit/b2b45af37f807068f6c23f152fe6e5bf64656915). 36 | The vulnerability is fixed by a [breaking change](https://github.com/ypnos-web/cakephp-datatables/commit/81929ad62d1e4041d00c1904f67771fec04ecd5f) 37 | in all branches in this repository. It affects the ordering and filtering functionality of DataTables in conjunction with 38 | server-side processing. If you are using a prior version of this plugin, you need to update it immediately and, if needed, change your code to 39 | [allow ordering and filtering with server-side processing](https://github.com/ypnos-web/cakephp-datatables/wiki/Quick-Start#enable-dynamic-filters-and-ordering). 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ypnos-web/cakephp-datatables", 3 | "description": "jQuery DataTables for CakePHP 5", 4 | "homepage": "https://github.com/ypnos-web/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 | "name": "Johannes Jordan", 16 | "homepage": "https://github.com/ypnos-web", 17 | "role": "Author" 18 | }, 19 | { 20 | "name": "Xavier ZOLEZZI", 21 | "homepage": "https://github.com/x-zolezzi", 22 | "role": "Author" 23 | }, 24 | { 25 | "name": "Umer Salman", 26 | "homepage": "https://github.com/umer936", 27 | "role": "Author" 28 | } 29 | ], 30 | "require": { 31 | "php": ">=8.1", 32 | "cakephp/cakephp": "^5.0" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "DataTables\\": "src" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "DataTables\\Test\\": "tests", 42 | "Cake\\Test\\": "./vendor/cakephp/cakephp/tests" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Controller/Component/DataTablesComponent.php: -------------------------------------------------------------------------------- 1 | 0, 23 | 'length' => 10, 24 | 'order' => [], 25 | 'prefixSearch' => true, // use "LIKE …%" instead of "LIKE %…%" conditions 26 | 'conditionsOr' => [], // table-wide search conditions 27 | 'conditionsAnd' => [], // column search conditions 28 | 'matching' => [], // column search conditions for foreign tables 29 | 'comparison' => [], // per-column comparison definition 30 | ]; 31 | 32 | protected $_defaultComparison = [ 33 | 'string' => 'LIKE', 34 | 'text' => 'LIKE', 35 | 'uuid' => 'LIKE', 36 | 'integer' => '=', 37 | 'biginteger' => '=', 38 | 'float' => '=', 39 | 'decimal' => '=', 40 | 'boolean' => '=', 41 | 'binary' => 'LIKE', 42 | 'date' => 'LIKE', 43 | 'datetime' => 'LIKE', 44 | 'timestamp' => 'LIKE', 45 | 'time' => 'LIKE', 46 | 'json' => 'LIKE', 47 | ]; 48 | 49 | protected $_viewVars = [ 50 | 'recordsTotal' => 0, 51 | 'recordsFiltered' => 0, 52 | 'draw' => 0 53 | ]; 54 | 55 | /** @var Table */ 56 | protected $_table = null; 57 | 58 | /** @var ColumnDefinitions */ 59 | protected $_columns = null; 60 | 61 | public function initialize(array $config): void 62 | { 63 | /* Set default comparison operators for field types */ 64 | if (Configure::check('DataTables.ComparisonOperators')) { 65 | $operators = Configure::read('DataTables.ComparisonOperators'); 66 | $this->_defaultComparison = array_merge($this->_defaultComparison, $operators); 67 | }; 68 | 69 | /* setup column definitions */ 70 | $this->_columns = new ColumnDefinitions(); 71 | } 72 | 73 | public function columns() 74 | { 75 | return $this->_columns; 76 | } 77 | 78 | /** 79 | * Process draw option (pass-through) 80 | */ 81 | private function _draw() 82 | { 83 | $drawParam = $this->getController()->getRequest()->getQuery('draw'); 84 | if (!$drawParam) 85 | return; 86 | 87 | $this->_viewVars['draw'] = (int)$drawParam; 88 | } 89 | 90 | /** 91 | * Process query data of ajax request regarding order 92 | * Alters $options if delegateOrder is set 93 | * In this case, the model needs to handle the 'customOrder' option. 94 | * @param $options: Query options 95 | * @param ColumnDefinitions|array Column definitions 96 | */ 97 | private function _order(array &$options, &$columns) 98 | { 99 | $queryParams = $this->getController()->getRequest()->getQueryParams(); 100 | 101 | if (empty($queryParams['order'])) 102 | return; 103 | 104 | $order = $this->getConfig('order'); 105 | /* extract custom ordering from request */ 106 | foreach ($queryParams['order'] as $item) { 107 | if (!count($columns)) // note: empty() does not work on objects 108 | throw new \InvalidArgumentException('Column ordering requested, but no column definitions provided.'); 109 | 110 | $dir = strtoupper($item['dir']); 111 | if (!in_array($dir, ['ASC', 'DESC'])) 112 | throw new BadRequestException('Malformed order direction.'); 113 | 114 | $c = $columns[$item['column']] ?? null; 115 | if (!$c || !($c['orderable'] ?? true)) // orderable is true by default 116 | throw new BadRequestException('Illegal column ordering.'); 117 | 118 | if (empty($c['field'])) 119 | throw new \InvalidArgumentException('Column description misses field name.'); 120 | 121 | $order[$c['field']] = $dir; 122 | } 123 | 124 | /* apply ordering */ 125 | if (!empty($options['delegateOrder'])) { 126 | $options['customOrder'] = $order; 127 | } else { 128 | $this->setConfig('order', $order); 129 | } 130 | 131 | /* remove default ordering in favor of our custom one */ 132 | unset($options['order']); 133 | } 134 | 135 | /** 136 | * Process query data of ajax request regarding filtering 137 | * Alters $options if delegateSearch is set 138 | * In this case, the model needs to handle the 'globalSearch' option. 139 | * @param $options: Query options 140 | * @param ColumnDefinitions|array $columns Column definitions 141 | * @return: true if additional filtering takes place 142 | */ 143 | private function _filter(array &$options, &$columns) : bool 144 | { 145 | $queryParams = $this->getController()->getRequest()->getQueryParams(); 146 | 147 | /* add limit and offset */ 148 | if (!empty($queryParams['length'])) { 149 | $this->setConfig('length', $queryParams['length']); 150 | } 151 | if (!empty($queryParams['start'])) { 152 | $this->setConfig('start', (int)$queryParams['start']); 153 | } 154 | 155 | $haveFilters = false; 156 | $delegateSearch = $options['delegateSearch'] ?? false; 157 | 158 | /* add global filter (general search field) */ 159 | $globalSearch = $queryParams['search']['value'] ?? ''; 160 | if ($globalSearch !== '') { 161 | if (empty($columns)) 162 | throw new \InvalidArgumentException('Filtering requested, but no column definitions provided.'); 163 | 164 | if ($delegateSearch) { 165 | $options['globalSearch'] = $globalSearch; 166 | $haveFilters = true; 167 | } else { 168 | foreach ($columns as $c) { 169 | if (!($c['searchable'] ?? true)) // searchable is true by default 170 | continue; 171 | 172 | if (empty($c['field'])) 173 | throw new \InvalidArgumentException('Column description misses field name.'); 174 | 175 | $this->_addCondition($c['field'], $globalSearch, 'or'); 176 | $haveFilters = true; 177 | } 178 | } 179 | } 180 | 181 | /* add local filters (column search fields) */ 182 | foreach ($queryParams['columns'] ?? [] as $index => $column) { 183 | $localSearch = $column['search']['value'] ?? ''; 184 | if ($localSearch !== '') { 185 | if (!count($columns)) // note: empty() does not work on objects 186 | throw new \InvalidArgumentException('Filtering requested, but no column definitions provided.'); 187 | 188 | $c = $columns[$index] ?? null; 189 | if (!$c || !($c['searchable'] ?? true)) // searchable is true by default 190 | throw new BadRequestException('Illegal filter request.'); 191 | 192 | if (empty($c['field'])) 193 | throw new \InvalidArgumentException('Column description misses field name.'); 194 | 195 | if ($delegateSearch) { 196 | $options['localSearch'][$c['field']] = $localSearch; 197 | } else { 198 | $this->_addCondition($c['field'], $localSearch); 199 | } 200 | $haveFilters = true; 201 | } 202 | } 203 | 204 | return $haveFilters; 205 | } 206 | 207 | /** 208 | * Find data 209 | * 210 | * @param $tableName: ORM table name 211 | * @param $finder: Finder name (as in Table::find()) 212 | * @param $options: Finder options (as in Table::find()) 213 | * @param $columns: Column definitions needed for filter/order operations 214 | * @return Query to be evaluated (Query::count() may have already been called) 215 | */ 216 | public function find(string $tableName, string $finder = 'all', array $options = [], array $columns = []) : Query 217 | { 218 | $delegateSearch = $options['delegateSearch'] ?? false; 219 | if (empty($columns)) 220 | $columns = $this->_columns; 221 | 222 | // -- get table object 223 | $this->_table = $this->getTableLocator()->get($tableName); 224 | 225 | // -- process draw & ordering options 226 | $this->_draw(); 227 | $this->_order($options, $columns); 228 | 229 | // -- call table's finder w/o filters 230 | $data = $this->_table->find($finder, $options); 231 | 232 | // -- retrieve total count 233 | $this->_viewVars['recordsTotal'] = $data->count(); 234 | 235 | // -- process filter options 236 | $haveFilters = $this->_filter($options, $columns); 237 | 238 | // -- apply filters 239 | if ($haveFilters) { 240 | if ($delegateSearch) { 241 | // call finder again to process filters (provided in $options) 242 | $data = $this->_table->find($finder, $options); 243 | } else { 244 | $data->where($this->getConfig('conditionsAnd')); 245 | foreach ($this->getConfig('matching') as $association => $where) { 246 | $data->matching($association, function (Query $q) use ($where) { 247 | return $q->where($where); 248 | }); 249 | } 250 | if (!empty($this->getConfig('conditionsOr'))) { 251 | $data->where(['or' => $this->getConfig('conditionsOr')]); 252 | } 253 | } 254 | } 255 | 256 | // -- retrieve filtered count 257 | $this->_viewVars['recordsFiltered'] = $data->count(); 258 | 259 | // -- add limit 260 | if ($this->getConfig('length') > 0) { // dt might provide -1 261 | $data->limit($this->getConfig('length')); 262 | $data->offset($this->getConfig('start')); 263 | } 264 | 265 | // -- sort 266 | $data->orderBy($this->getConfig('order')); 267 | 268 | // -- set all view vars to view and serialize array 269 | $this->_setViewVars(); 270 | return $data; 271 | 272 | } 273 | 274 | private function _setViewVars() 275 | { 276 | $controller = $this->getController(); 277 | 278 | $_serialize = $controller->viewBuilder()->getVar('_serialize') ?? []; 279 | $_serialize = array_merge($_serialize, array_keys($this->_viewVars)); 280 | 281 | $controller->set($this->_viewVars); 282 | $controller->set('_serialize', $_serialize); 283 | } 284 | 285 | private function _addCondition($column, $value, $type = 'and') 286 | { 287 | /* extract table (encoded in $column or default) */ 288 | $table = $this->_table; 289 | if (($pos = strpos($column, '.')) !== false) { 290 | $table = $this->getTableLocator()->get(substr($column, 0, $pos)); 291 | $column = substr($column, $pos + 1); 292 | } 293 | 294 | $textCast = ""; 295 | 296 | /* build condition */ 297 | $comparison = trim($this->_getComparison($table, $column)); 298 | 299 | $columnDesc = $table->getSchema()->getColumn($column); 300 | $columnType = $columnDesc['type']; 301 | // wrap value for LIKE and NOT LIKE 302 | if (strpos(strtolower($comparison), 'like') !== false) { 303 | $value = $this->getConfig('prefixSearch') ? "{$value}%" : "%{$value}%"; 304 | 305 | if ($this->_table->getConnection()->getDriver() instanceof Postgres) { 306 | if ($columnType !== 'string' && $columnType !== 'text') { 307 | $textCast = "::text"; 308 | } 309 | } 310 | } 311 | settype($value, $columnType); 312 | $condition = ["{$table->getAlias()}.{$column}{$textCast} {$comparison}" => $value]; 313 | 314 | /* add as global condition */ 315 | if ($type === 'or') { 316 | $this->setConfig('conditionsOr', $condition); // merges 317 | return; 318 | } 319 | 320 | /* add as local condition */ 321 | if ($table === $this->_table) { 322 | $this->setConfig('conditionsAnd', $condition); // merges 323 | } else { 324 | $this->setConfig('matching', [$table->getAlias() => $condition]); // merges 325 | } 326 | } 327 | 328 | /** 329 | * Get comparison operator by entity and column name. 330 | * 331 | * @param $table: Target ORM table 332 | * @param $column: Database column name (may be in form Table.column) 333 | * @return: Database comparison operator 334 | */ 335 | protected function _getComparison(Table $table, string $column) : string 336 | { 337 | $config = new Collection($this->getConfig('comparison')); 338 | 339 | /* Lookup per-column configuration for the comparison operator */ 340 | $userConfig = $config->filter(function ($item, $key) use ($table, $column) { 341 | $wanted = sprintf('%s.%s', $table->getAlias(), $column); 342 | return strtolower($key) === strtolower($wanted); 343 | }); 344 | if (!$userConfig->isEmpty()) 345 | return $userConfig->first(); 346 | 347 | /* Lookup per-field type configuration for the comparison operator */ 348 | $columnDesc = $table->getSchema()->getColumn($column); 349 | return $this->_defaultComparison[$columnDesc['type']] ?? '='; 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /src/Lib/CallbackFunction.php: -------------------------------------------------------------------------------- 1 | $content) { 28 | $replacements[$id] = strtr($content, self::$_placeholders); 29 | } 30 | return strtr($json, $replacements); 31 | } 32 | 33 | /** 34 | * Holds this specific object's hash to be passed in jsonSerialize() 35 | * @var string 36 | */ 37 | protected $hash; 38 | 39 | /** 40 | * CallbackFunction constructor. 41 | * @param string $name Name of Javascript function to call 42 | * @param array $args Optional array of arguments to be passed when calling 43 | */ 44 | public function __construct(string $name, array $args = []) 45 | { 46 | if (empty($args)) { 47 | $code = $name; 48 | } else { 49 | $code = 'function () { '; 50 | foreach ($args as $a) { 51 | $arg = json_encode($a); 52 | $code .= "Array.prototype.push.call(arguments, {$arg});"; 53 | } 54 | $code .= "return {$name}.apply(this, arguments); }"; 55 | } 56 | 57 | $this->setHash($code); 58 | } 59 | 60 | /** 61 | * Set hash for this wrapper and register in placeholder list 62 | * @param $code: payload 63 | */ 64 | protected function setHash(string $code) 65 | { 66 | $this->hash = md5($code); 67 | // use parenthesis as this is how it will show up in json 68 | self::$_placeholders['"'.$this->hash.'"'] = $code; 69 | } 70 | 71 | /** 72 | * Get code generated for this JS function call 73 | * @return string the generated code 74 | */ 75 | public function code() : string 76 | { 77 | return self::$_placeholders['"'.$this->hash.'"']; 78 | } 79 | 80 | /** 81 | * Serialize to a placeholder in json 82 | * @return: a unique hash to be replaced by resolve() after json_encode() 83 | */ 84 | public function jsonSerialize() : string 85 | { 86 | return $this->hash; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Lib/ColumnDefinition.php: -------------------------------------------------------------------------------- 1 | content = $template; 29 | $this->owner = $owner; 30 | 31 | $this->switchesNegative = array_map(function ($e) { 32 | return 'not'.ucfirst($e); 33 | }, $this->switchesPositive); 34 | } 35 | 36 | /** 37 | * Refer back to owner's add() 38 | * A convenient way to add another column 39 | */ 40 | public function add(...$args) : ColumnDefinition 41 | { 42 | return $this->owner->add(...$args); 43 | } 44 | 45 | /** 46 | * Set one or many properties 47 | * @param $key string|array If array given, it should be key -> value 48 | * @param $value: The singular value to set, if string $key given 49 | * @return ColumnDefinition 50 | */ 51 | public function set($key, $value = null) : ColumnDefinition 52 | { 53 | if (is_array($key)) { 54 | if (!empty($value)) 55 | throw new \InvalidArgumentException("Provide either array or key/value pair!"); 56 | 57 | $this->content = $key + $this->content; 58 | } else { 59 | $this->content[$key] = $value; 60 | } 61 | return $this; 62 | } 63 | 64 | /* provide some convenience wrappers for set() */ 65 | public function __call($name, $arguments) : ColumnDefinition 66 | { 67 | if (in_array($name, $this->switchesPositive)) { 68 | if (!empty($arguments)) 69 | throw new \InvalidArgumentException("$name() takes no arguments!"); 70 | 71 | $this->content[$name] = true; 72 | } 73 | if (in_array($name, $this->switchesNegative)) { 74 | if (!empty($arguments)) 75 | throw new \InvalidArgumentException("$name() takes no arguments!"); 76 | 77 | $name = lcfirst(substr($name, 3)); 78 | $this->content[$name] = false; 79 | } 80 | 81 | return $this; 82 | } 83 | 84 | public function unset(string $key) : ColumnDefinition 85 | { 86 | unset($this->content[$key]); 87 | return $this; 88 | } 89 | 90 | /** 91 | * @param $name: see CallbackFunction::__construct 92 | * @param $args: see CallbackFunction::__construct 93 | * @return ColumnDefinition 94 | */ 95 | public function render(string $name, array $args = []) : ColumnDefinition 96 | { 97 | $this->content['render'] = new CallbackFunction($name, $args); 98 | return $this; 99 | } 100 | 101 | public function jsonSerialize() : array 102 | { 103 | return $this->content; 104 | } 105 | 106 | public function offsetExists($offset) 107 | { 108 | return isset($this->content[$offset]); 109 | } 110 | 111 | public function offsetGet($offset) 112 | { 113 | return $this->content[$offset]; 114 | } 115 | 116 | public function offsetSet($offset, $value) 117 | { 118 | if (is_null($offset)) { 119 | $this->content[] = $value; 120 | } else { 121 | $this->content[$offset] = $value; 122 | } 123 | } 124 | 125 | public function offsetUnset($offset) 126 | { 127 | unset($this->content[$offset]); 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/Lib/ColumnDefinitions.php: -------------------------------------------------------------------------------- 1 | $column, 25 | 'data' => $column, // a good guess (user can adjust it later) 26 | ]; 27 | if ($fieldname) 28 | $column['field'] = $fieldname; 29 | 30 | $column = new ColumnDefinition($column, $this); 31 | $this->store($column); 32 | 33 | return $column; 34 | } 35 | 36 | /** 37 | * Set titles of columns in given order 38 | * Convenience method for setting all titles at once 39 | * @param $titles array of titles in order of columns 40 | */ 41 | public function setTitles(array $titles) 42 | { 43 | if (count($titles) != count($this->columns)) { 44 | $msg = 'Have ' . count($this->columns) . ' columns, but ' . count($titles) . ' titles given!'; 45 | throw new \InvalidArgumentException($msg); 46 | } 47 | foreach ($titles as $i => $t) { 48 | if (!empty($t)) 49 | $this->columns[$i]['title'] = $t; 50 | } 51 | } 52 | 53 | /** 54 | * Serialize to an array in json 55 | * @return: column definitions 56 | */ 57 | public function jsonSerialize() : array 58 | { 59 | return array_values($this->columns); 60 | } 61 | 62 | public function offsetExists($offset) : bool 63 | { 64 | if (is_numeric($offset)) 65 | return isset($this->columns[$offset]); 66 | return isset($this->index[$offset]); 67 | } 68 | 69 | public function offsetGet(mixed $offset): mixed 70 | { 71 | if (is_numeric($offset)) 72 | return $this->columns[$offset]; 73 | return $this->columns[$this->index[$offset]]; 74 | } 75 | 76 | public function offsetSet(mixed $offset, mixed $value): void 77 | { 78 | throw new \BadMethodCallException('Direct setting is not supported! Use add().'); 79 | } 80 | 81 | public function offsetUnset(mixed $offset): void 82 | { 83 | /* we do not allow splicing because DataTables uses a column's index 84 | for the ordering command. So the order of columns needs to stay 85 | consistent from the Controller down to the table displayed. */ 86 | throw new \BadMethodCallException('Unset is not supported!'); 87 | } 88 | 89 | public function getIterator(): Traversable 90 | { 91 | return new \ArrayIterator($this->columns); 92 | } 93 | 94 | public function count(): int 95 | { 96 | return count($this->columns); 97 | } 98 | 99 | protected function store(ColumnDefinition $column) 100 | { 101 | $this->columns[] = $column; 102 | /* keep track of where we stored it. 103 | Note: our array is only growing! No splicing! */ 104 | $this->index[$column['name']] = count($this->columns) - 1; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Locale/de/data_tables.po: -------------------------------------------------------------------------------- 1 | # German translation of DataTables plugin 2 | # Copyright 2016 Johannes Jordan 3 | # See https://datatables.net/plug-ins/i18n/German 4 | # 5 | msgid "" 6 | msgstr "" 7 | "POT-Creation-Date: 2016-03-29 18:49+0200\n" 8 | "Last-Translator: Johannes Jordan \n" 9 | "Language: de\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | "X-Generator: Poedit 1.8.7\n" 15 | 16 | #: View/Helper/DataTablesHelper.php:31 17 | msgid "No data available in table" 18 | msgstr "Keine Daten in der Tabelle vorhanden" 19 | 20 | #: View/Helper/DataTablesHelper.php:32 21 | msgid "Showing _START_ to _END_ of _TOTAL_ entries" 22 | msgstr "_START_ bis _END_ von _TOTAL_ Einträgen" 23 | 24 | #: View/Helper/DataTablesHelper.php:33 25 | msgid "No entries to show" 26 | msgstr "Keine Einträge" 27 | 28 | #: View/Helper/DataTablesHelper.php:34 29 | msgid "(filtered from _MAX_ total entries)" 30 | msgstr "(gefiltert von _MAX_ Einträgen)" 31 | 32 | #: View/Helper/DataTablesHelper.php:35 33 | msgid "Show _MENU_ entries" 34 | msgstr "_MENU_ Einträge anzeigen" 35 | 36 | #: View/Helper/DataTablesHelper.php:36 37 | msgid "Processing..." 38 | msgstr "Bitte warten..." 39 | 40 | #: View/Helper/DataTablesHelper.php:37 41 | msgid "Search:" 42 | msgstr "Suche:" 43 | 44 | #: View/Helper/DataTablesHelper.php:38 45 | msgid "No matching records found" 46 | msgstr "Keine Einträge vorhanden." 47 | 48 | #: View/Helper/DataTablesHelper.php:40 49 | msgid "First" 50 | msgstr "Erste" 51 | 52 | #: View/Helper/DataTablesHelper.php:41 53 | msgid "Last" 54 | msgstr "Letzte" 55 | 56 | #: View/Helper/DataTablesHelper.php:42 57 | msgid "Next" 58 | msgstr "Zurück" 59 | 60 | #: View/Helper/DataTablesHelper.php:43 61 | msgid "Previous" 62 | msgstr "Nächste" 63 | 64 | #: View/Helper/DataTablesHelper.php:46 65 | msgid ": activate to sort column ascending" 66 | msgstr ": aktivieren, um Spalte aufsteigend zu sortieren" 67 | 68 | #: View/Helper/DataTablesHelper.php:47 69 | msgid ": activate to sort column descending" 70 | msgstr ": aktivieren, um Spalte absteigend zu sortieren" 71 | 72 | #~ msgid "Display {0} records" 73 | #~ msgstr "Zeige {0} Einträge" 74 | -------------------------------------------------------------------------------- /src/View/Helper/DataTablesHelper.php: -------------------------------------------------------------------------------- 1 | true, 18 | 'processing' => true, 19 | 'serverSide' => true, 20 | 'deferRender' => true, 21 | ]; 22 | 23 | public function initialize(array $config) : void 24 | { 25 | /* set default i18n (not possible in _$defaultConfig due to use of __d() */ 26 | if (empty($this->getConfig('language'))) { 27 | // defaults from datatables.net/reference/option/language 28 | $this->setConfig('language', [ 29 | 'emptyTable' => __d('data_tables', 'No data available in table'), 30 | 'info' => __d('data_tables', 'Showing _START_ to _END_ of _TOTAL_ entries'), 31 | 'infoEmpty' => __d('data_tables', 'No entries to show'), 32 | 'infoFiltered' => __d('data_tables', '(filtered from _MAX_ total entries)'), 33 | 'lengthMenu' => __d('data_tables', 'Show _MENU_ entries'), 34 | 'processing' => __d('data_tables', 'Processing...'), 35 | 'search' => __d('data_tables', 'Search:'), 36 | 'zeroRecords' => __d('data_tables', 'No matching records found'), 37 | 'paginate' => [ 38 | 'first' => __d('data_tables', 'First'), 39 | 'last' => __d('data_tables', 'Last'), 40 | 'next' => __d('data_tables', 'Next'), 41 | 'previous' => __d('data_tables', 'Previous'), 42 | ], 43 | 'aria' => [ 44 | 'sortAscending' => __d('data_tables', ': activate to sort column ascending'), 45 | 'sortDescending' => __d('data_tables', ': activate to sort column descending'), 46 | ], 47 | ]); 48 | } 49 | } 50 | /** 51 | * Return a Javascript function wrapper to be used in DataTables configuration 52 | * @param string $name Name of Javascript function to call 53 | * @param array $args Optional array of arguments to be passed when calling 54 | * @return CallbackFunction 55 | */ 56 | public function callback(string $name, array $args = []) : CallbackFunction 57 | { 58 | return new CallbackFunction($name, $args); 59 | } 60 | 61 | /** 62 | * Return a table with dataTables overlay 63 | * @param $id: DOM id of the table 64 | * @param $dtOptions: Options for DataTables (to be merged with this helper's config as defaults) 65 | * @param $htmlOptions: Options for the table, e.g. CSS classes 66 | * @return string containing a and a