├── .gitignore ├── README.md ├── composer.json └── src ├── Config.php ├── DataTables.php ├── DataTablesCodeIgniter3.php ├── DataTablesCodeIgniter4.php ├── Helper.php └── functions.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | tests 3 | composer.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeIgniter DataTables 2 | 3 | DataTables server-side for CodeIgniter, supported for both CodeIgniter 3 and CodeIgniter 4. 4 | 5 | **Note:** This library only handle the server-side part, you will still need to set up the client-side components, such as jQuery, the DataTables library, and the necessary styles. **Don't worry, we've included examples below to help you get started.** 6 | 7 | ## Requirements 8 | 9 | No additional requirements are needed if you are already using CodeIgniter. Simply integrate this library into your existing project. 10 | 11 | ## Installation 12 | 13 | To install the library, use Composer. This command will handle the installation process for you: 14 | 15 | ```sh 16 | composer require ngekoding/codeigniter-datatables 17 | ``` 18 | 19 | ## Usage 20 | 21 | Below is a basic example of how to use this library. Feel free to customize the client-side configuration, such as defining searchable columns, orderable columns, and other DataTables options. 22 | 23 | The usage of this library is similar for both CodeIgniter 3 and CodeIgniter 4. **The primary difference is in how you create the query builder.** Below are examples for both versions. 24 | 25 | ### CodeIgniter 3 Example 26 | 27 | ```php 28 | // CodeIgniter 3 Example 29 | 30 | // Here we will select all fields from posts table 31 | // and make a join with categories table 32 | // IMPORTANT! We don't need to call ->get() here 33 | $queryBuilder = $this->db->select('p.*, c.name category') 34 | ->from('posts p') 35 | ->join('categories c', 'c.id=p.category_id'); 36 | 37 | // The library will automatically detect the CodeIgniter version you are using 38 | $datatables = new Ngekoding\CodeIgniterDataTables\DataTables($queryBuilder); 39 | $datatables->generate(); // done 40 | ``` 41 | 42 | ### CodeIgniter 4 Example 43 | 44 | ```php 45 | // CodeIgniter 4 Example 46 | 47 | $db = db_connect(); 48 | $queryBuilder = $db->from('posts p') 49 | ->select('p.*, c.name category') 50 | ->join('categories c', 'c.id=p.category_id'); 51 | 52 | // The library will automatically detect the CodeIgniter version you are using 53 | $datatables = new Ngekoding\CodeIgniterDataTables\DataTables($queryBuilder); 54 | $datatables->generate(); // done 55 | ``` 56 | 57 | **The above examples will give you for [ajax data source (arrays)](https://datatables.net/examples/ajax/simple.html), so you need to make sure the table header you makes for the client side is match with the ajax response. We will talk about the objects data source below.** 58 | 59 | ### Client Side Examples 60 | 61 | You must include the jQuery and DataTables library. 62 | 63 | ```html 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
IDTitleCategoryDescription
74 | 75 | 76 | 77 | 87 | ``` 88 | 89 | ## Objects Data Source 90 | 91 | As was mentioned above, the default data source we get is an arrays. It is easy also to get the objects data source. 92 | 93 | To get objects response, you just need to call `asObject()` method. 94 | 95 | ```php 96 | $datatables->asObject() 97 | ->generate(); 98 | ``` 99 | 100 | And then you can configure the client side with columns option to fit your data. 101 | 102 | ```js 103 | $('#table-post').DataTable({ 104 | processing: true, 105 | serverSide: true, 106 | ajax: { 107 | url: 'http://localhost/project/index.php/post/ajax_datatables', 108 | method: 'GET', 109 | }, 110 | columns: [ 111 | { data: 'id' }, 112 | { data: 'title' }, 113 | { data: 'category' }, 114 | { data: 'description' } 115 | ] 116 | }) 117 | 118 | ``` 119 | 120 | ## Some Others Settings 121 | 122 | Some basic functionalities already available, here is the full settings you can doing to this library. 123 | 124 | ### Use class for spesify the CodeIgniter version 125 | ```php 126 | // General, use the second param to define the version (3 or 4) 127 | $datatables = new Ngekoding\CodeIgniterDataTables\DataTables($queryBuilder, 3); 128 | 129 | // CodeIgniter 3 130 | $datatables = new Ngekoding\CodeIgniterDataTables\DataTablesCodeIgniter3($queryBuilder); 131 | 132 | // CodeIgniter 4 133 | $datatables = new Ngekoding\CodeIgniterDataTables\DataTablesCodeIgniter4($queryBuilder); 134 | 135 | ``` 136 | 137 | ### Available Options 138 | 139 | ```php 140 | $datatables = new Ngekoding\CodeIgniterDataTables\DataTables($queryBuilder); 141 | 142 | // Return the output as objects instead of arrays 143 | $datatables->asObject(); 144 | 145 | // Only return title & category (accept string or array) 146 | $datatables->only(['title', 'category']); 147 | 148 | // Return all except the id 149 | // You may use one of only or except 150 | $datatables->except(['id']); 151 | 152 | // Format the output 153 | $datatables->format('title', function($value, $row) { 154 | return ''.$value.''; 155 | }); 156 | 157 | // Add extra column 158 | $datatables->addColumn('action', function($row) { 159 | return 'Delete'; 160 | }); 161 | 162 | // Add column alias 163 | // It is very useful when we use SELECT JOIN to prevent column ambiguous 164 | $datatables->addColumnAlias('p.id', 'id'); 165 | 166 | // Add column aliases 167 | // Same as the addColumnAlias, but for multiple alias at once 168 | $datatables->addColumnAliases([ 169 | 'p.id' => 'id', 170 | 'c.name' => 'category' 171 | ]); 172 | 173 | // Add squence number 174 | // The default key is `sequenceNumber` 175 | // You can change it with give the param 176 | $datatables->addSequenceNumber(); 177 | $datatables->addSequenceNumber('rowNumber'); // It will be rowNumber 178 | 179 | // Don't forget to call generate to get the results 180 | $datatables->generate(); 181 | ``` 182 | 183 | ## Complete Example 184 | 185 | I already use this library to the existing project with completed CRUD operations, you can found it [here](https://github.com/ngekoding/ci-crud). 186 | 187 |
188 | Please look at these files: 189 | 190 | - application/composer.json 191 | - application/controllers/Post.php 192 | - application/models/M_post.php 193 | - application/views/template.php 194 | - application/views/posts/index-datatables.php 195 | - application/views/posts/index-datatables-array.php 196 | - application/helpers/api_helper.php 197 | - assets/js/custom.js 198 | 199 |
-------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngekoding/codeigniter-datatables", 3 | "description": "DataTables server-side for CodeIgniter", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Nur Muhammad", 9 | "email": "blog.nurmuhammad@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.6", 14 | "symfony/http-foundation": "*", 15 | "greenlion/php-sql-parser": "^4.5" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "Ngekoding\\CodeIgniterDataTables\\": "src" 20 | }, 21 | "files": [ 22 | "src/functions.php" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'countAllResults' => 'count_all_results', 16 | 'orderBy' => 'order_by', 17 | 'where' => 'where', 18 | 'like' => 'like', 19 | 'orLike' => 'or_like', 20 | 'limit' => 'limit', 21 | 'get' => 'get', 22 | 'QBSelect' => 'qb_select', 23 | 'getFieldNames' => 'list_fields', 24 | 'getResult' => 'result', 25 | 'getResultArray' => 'result_array', 26 | 'getCompiledSelect' => 'get_compiled_select', 27 | 'groupStart' => 'group_start', 28 | 'groupEnd' => 'group_end' 29 | ] 30 | ]; 31 | 32 | public function __construct($ciVersion = '4') 33 | { 34 | $this->ciVersion = $ciVersion; 35 | } 36 | 37 | public function get($name) 38 | { 39 | if (isset($this->methodsMapping[$this->ciVersion])) { 40 | return $this->methodsMapping[$this->ciVersion][$name]; 41 | } 42 | return $name; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/DataTables.php: -------------------------------------------------------------------------------- 1 | ciVersion = Helper::resolveCodeIgniterVersion($ciVersion); 40 | $this->config = new Config($this->ciVersion); 41 | $this->request = Request::createFromGlobals(); 42 | $this->queryBuilder = $queryBuilder; 43 | 44 | $this->columnAliases = Helper::getColumnAliases($queryBuilder, $this->config); 45 | 46 | // When getting the field names, the query builder will be changed 47 | // So we need to make a clone to keep the original 48 | $queryBuilderClone = clone $queryBuilder; 49 | $this->fieldNames = Helper::getFieldNames($queryBuilderClone, $this->config); 50 | 51 | $this->recordsTotal = $this->queryBuilder->{$this->config->get('countAllResults')}('', FALSE); 52 | } 53 | 54 | /** 55 | * Format the value of spesific key 56 | * @param string $key The key to formatted 57 | * @param function $callback The formatter callback 58 | * 59 | * @return $this 60 | */ 61 | public function format($key, $callback) 62 | { 63 | $this->formatters[$key] = $callback; 64 | return $this; 65 | } 66 | 67 | /** 68 | * Add extra column 69 | * @param string $key The key of the column 70 | * @param $callback The extra column callback, like a formatter 71 | * 72 | * @return $this 73 | */ 74 | public function addColumn($key, $callback) 75 | { 76 | $this->extraColumns[$key] = $callback; 77 | return $this; 78 | } 79 | 80 | /** 81 | * Add column alias 82 | * Very useful when using SELECT JOIN to prevent column ambiguous 83 | * @param string $key The key of the column name (e.g. including the table name: posts.id or p.id) 84 | * @param string $alias The column alias 85 | * 86 | * @return $this 87 | */ 88 | public function addColumnAlias($key, $alias) 89 | { 90 | $this->columnAliases[$alias] = $key; 91 | return $this; 92 | } 93 | 94 | /** 95 | * Add multiple column alias 96 | * @param array $aliases The column aliases with key => value format 97 | * 98 | * @return $this 99 | */ 100 | public function addColumnAliases($aliases) 101 | { 102 | if ( ! is_array($aliases)) { 103 | throw new \Exception('The $aliases parameter must be an array.'); 104 | } 105 | 106 | foreach ($aliases as $key => $alias) { 107 | $this->addColumnAlias($key, $alias); 108 | } 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * Only return the column as defined 115 | * @param string|array $columns The columns to only will be returned 116 | * 117 | * @return $this 118 | */ 119 | public function only($columns) 120 | { 121 | if (is_array($columns)) { 122 | array_push($this->only, ...$columns); 123 | } else { 124 | array_push($this->only, $columns); 125 | } 126 | return $this; 127 | } 128 | 129 | /** 130 | * Return all column except this 131 | * @param string|array $columns The columns to except 132 | * 133 | * @return $this 134 | */ 135 | public function except($columns) 136 | { 137 | if (is_array($columns)) { 138 | array_push($this->except, ...$columns); 139 | } else { 140 | array_push($this->except, $columns); 141 | } 142 | return $this; 143 | } 144 | 145 | /** 146 | * Set the returned field names base on only & except 147 | * We will use the only first if defined 148 | * So you must use either only or except (not both) 149 | */ 150 | protected function setReturnedFieldNames() 151 | { 152 | if ( ! empty($this->only)) { 153 | // Keep fields order as defined 154 | foreach ($this->only as $field) { 155 | if (in_array($field, $this->fieldNames)) { 156 | array_push($this->returnedFieldNames, $field); 157 | } 158 | } 159 | } elseif ( ! empty($this->except)) { 160 | foreach ($this->fieldNames as $field) { 161 | if ( ! in_array($field, $this->except)) { 162 | array_push($this->returnedFieldNames, $field); 163 | } 164 | } 165 | } else { 166 | $this->returnedFieldNames = $this->fieldNames; 167 | } 168 | } 169 | 170 | /** 171 | * Resolves the column name 172 | * 173 | * @param int|string $column The column index or name, depending on `asObject` 174 | * @return string|null The resolved column name or NULL if out of bounds 175 | */ 176 | protected function resolveColumnName($column) 177 | { 178 | if ( ! $this->asObject) { 179 | $fieldIndex = $this->sequenceNumber ? $column - 1 : $column; 180 | 181 | // Skip sequence number and extra column 182 | if ( 183 | ($this->sequenceNumber && $column == 0) OR 184 | $fieldIndex > count($this->returnedFieldNames) - 1 185 | ) return NULL; 186 | 187 | $column = $this->returnedFieldNames[$fieldIndex]; 188 | } 189 | 190 | // Checking if it using a column alias 191 | $column = isset($this->columnAliases[$column]) 192 | ? $this->columnAliases[$column] 193 | : $column; 194 | 195 | return $column; 196 | } 197 | 198 | /** 199 | * Add sequence number to the output 200 | * @param string $key Used when returning object output as the key 201 | */ 202 | public function addSequenceNumber($key = 'sequenceNumber') 203 | { 204 | $this->sequenceNumber = TRUE; 205 | $this->sequenceNumberKey = $key; 206 | return $this; 207 | } 208 | 209 | /** 210 | * Run the filter query both for global & individual filter 211 | */ 212 | protected function filter() 213 | { 214 | $globalSearch = []; 215 | 216 | // Global column filtering 217 | if ($this->request->get('search') && ($keyword = $this->request->get('search')['value']) != '') { 218 | foreach ($this->request->get('columns', []) as $request_column) { 219 | if (filter_var($request_column['searchable'], FILTER_VALIDATE_BOOLEAN)) { 220 | $column = $request_column['data']; 221 | $column = $this->resolveColumnName($column); 222 | 223 | if (empty($column)) continue; 224 | 225 | $globalSearch[] = $column; 226 | } 227 | } 228 | } 229 | 230 | // Apply global search criteria 231 | if (!empty($globalSearch)) { 232 | $this->queryBuilder->{$this->config->get('groupStart')}(); 233 | 234 | foreach ($globalSearch as $column) { 235 | $this->queryBuilder->{$this->config->get('orLike')}($column, $keyword); 236 | } 237 | 238 | $this->queryBuilder->{$this->config->get('groupEnd')}(); 239 | } 240 | 241 | // Individual column filtering 242 | foreach ($this->request->get('columns', []) as $request_column) { 243 | if ( 244 | filter_var($request_column['searchable'], FILTER_VALIDATE_BOOLEAN) && 245 | ($keyword = $request_column['search']['value']) != '' 246 | ) { 247 | $column = $request_column['data']; 248 | $column = $this->resolveColumnName($column); 249 | 250 | if (empty($column)) continue; 251 | 252 | // Apply column-specific search criteria 253 | $this->queryBuilder->{$this->config->get('like')}($column, $keyword); 254 | } 255 | } 256 | 257 | $this->recordsFiltered = $this->queryBuilder->{$this->config->get('countAllResults')}('', FALSE); 258 | } 259 | 260 | /** 261 | * Run the order query 262 | */ 263 | protected function order() 264 | { 265 | if ($this->request->get('order') && count($this->request->get('order'))) { 266 | foreach ($this->request->get('order') as $order) { 267 | $column_idx = $order['column']; 268 | $request_column = $this->request->get('columns')[$column_idx]; 269 | 270 | if (filter_var($request_column['orderable'], FILTER_VALIDATE_BOOLEAN)) { 271 | $column = $request_column['data']; 272 | $column = $this->resolveColumnName($column); 273 | 274 | if (empty($column)) continue; 275 | 276 | // Apply order 277 | $this->queryBuilder->{$this->config->get('orderBy')}($column, $order['dir']); 278 | } 279 | } 280 | } 281 | } 282 | 283 | /** 284 | * Run the limit query for paginating 285 | */ 286 | protected function limit() 287 | { 288 | if (($start = $this->request->get('start')) !== NULL && ($length = $this->request->get('length')) != -1) { 289 | $this->queryBuilder->{$this->config->get('limit')}($length, $start); 290 | } 291 | } 292 | 293 | /** 294 | * Define the result as objects instead of arrays 295 | * 296 | * @return $this 297 | */ 298 | public function asObject() 299 | { 300 | $this->asObject = TRUE; 301 | return $this; 302 | } 303 | 304 | /** 305 | * Generate the datatables results 306 | */ 307 | public function generate() 308 | { 309 | $this->setReturnedFieldNames(); 310 | $this->filter(); 311 | $this->order(); 312 | $this->limit(); 313 | 314 | $result = $this->queryBuilder->{$this->config->get('get')}(); 315 | 316 | $output = []; 317 | 318 | $sequenceNumber = $this->request->get('start') + 1; 319 | foreach ($result->{$this->config->get('getResult')}() as $res) { 320 | // Add sequence number if needed 321 | if ($this->sequenceNumber) { 322 | $row[$this->sequenceNumberKey] = $sequenceNumber++; 323 | } 324 | 325 | foreach ($this->returnedFieldNames as $field) { 326 | $row[$field] = isset($this->formatters[$field]) 327 | ? $this->formatters[$field]($res->$field, $res) 328 | : $res->$field; 329 | } 330 | 331 | // Add extra columns 332 | foreach ($this->extraColumns as $key => $callback) { 333 | $row[$key] = $callback($res); 334 | } 335 | 336 | if ($this->asObject) { 337 | $output[] = (object) $row; 338 | } else { 339 | $output[] = array_values($row); 340 | } 341 | } 342 | 343 | $response = new JsonResponse(); 344 | $response->setData([ 345 | 'draw' => $this->request->get('draw'), 346 | 'recordsTotal' => $this->recordsTotal, 347 | 'recordsFiltered' => $this->recordsFiltered, 348 | 'data' => $output 349 | ]); 350 | $response->send(); 351 | exit; 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/DataTablesCodeIgniter3.php: -------------------------------------------------------------------------------- 1 | {$config->get('getCompiledSelect')}(); 13 | 14 | $parser = new PHPSQLParser(); 15 | $parsed = $parser->parse($compiledSelect); 16 | 17 | $tableAliases = []; 18 | foreach ($parsed['FROM'] as $from) { 19 | if ($from['expr_type'] === 'table') { 20 | $name = $from['no_quotes']['parts'][0]; 21 | $alias = isset($from['alias']['name']) ? $from['alias']['no_quotes']['parts'][0] : $name; 22 | $tableAliases[$alias] = $name; 23 | } 24 | } 25 | 26 | $columnAliases = []; 27 | foreach ($parsed['SELECT'] as $select) { 28 | $expr_type = $select['expr_type']; 29 | $base_expr = $select['base_expr']; 30 | 31 | if (isset($select['alias']['name'])) { 32 | $alias = $select['alias']['no_quotes']['parts'][0]; 33 | 34 | if ($expr_type === 'colref') { 35 | $key = implode('.', $select['no_quotes']['parts']); 36 | } elseif ($expr_type === 'expression') { 37 | $key = trim(str_replace($alias, '', $base_expr)); 38 | } elseif (str_contains($expr_type, 'function')) { 39 | $parts = []; 40 | foreach ($select['sub_tree'] as $part) { 41 | $parts[] = $part['base_expr']; 42 | } 43 | $key = $base_expr . '(' . implode(', ', $parts) . ')'; 44 | } 45 | $columnAliases[$alias] = $key; 46 | } elseif ($expr_type === 'colref') { 47 | if (str_contains($base_expr, '*')) { 48 | if (str_contains($base_expr, '.')) { 49 | $tableAlias = $select['no_quotes']['parts'][0]; 50 | } else { 51 | $tableAlias = array_key_first($tableAliases); 52 | } 53 | 54 | $tableName = $tableAliases[$tableAlias]; 55 | $fields = $queryBuilder->{$config->get('getFieldNames')}($tableName); 56 | foreach ($fields as $field) { 57 | $key = "{$tableAlias}.{$field}"; 58 | $columnAliases[$field] = $key; 59 | } 60 | } elseif (str_contains($base_expr, '.')) { 61 | $field = $select['no_quotes']['parts'][1]; 62 | $key = implode('.', $select['no_quotes']['parts']); 63 | $columnAliases[$field] = $key; 64 | } 65 | } 66 | } 67 | 68 | return $columnAliases; 69 | } 70 | 71 | /** 72 | * Get all select fields result 73 | * Used when we use the arrays data source for ordering 74 | * @param $queryBuilder 75 | * @param Config $config 76 | * 77 | * @return array 78 | */ 79 | public static function getFieldNames($queryBuilder, $config) 80 | { 81 | return $queryBuilder->where('0=1') // We don't need any data 82 | ->{$config->get('get')}() 83 | ->{$config->get('getFieldNames')}(); 84 | } 85 | 86 | /** 87 | * Resolve CodeIgniter version 88 | * 89 | * @param string|int|null $ciVersion 90 | * @return int 91 | */ 92 | public static function resolveCodeIgniterVersion($ciVersion) 93 | { 94 | if ( ! in_array($ciVersion, [3, 4])) { 95 | if (class_exists(\CodeIgniter\Database\BaseBuilder::class)) { 96 | return 4; 97 | } 98 | return 3; 99 | } 100 | return (int) $ciVersion; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 |