├── .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 |
ID
69 |
Title
70 |
Category
71 |
Description
72 |
73 |
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 |