1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace AdMos\DataTables; |
4
|
|
|
|
5
|
|
|
use Carbon\Carbon; |
6
|
|
|
use Doctrine\DBAL\Schema\Column; |
7
|
|
|
use Doctrine\DBAL\Types\BigIntType; |
8
|
|
|
use Doctrine\DBAL\Types\IntegerType; |
9
|
|
|
use Doctrine\DBAL\Types\SmallIntType; |
10
|
|
|
use Illuminate\Database\DatabaseManager; |
11
|
|
|
use Illuminate\Database\Eloquent\Builder; |
12
|
|
|
use Illuminate\Database\Eloquent\Model; |
13
|
|
|
use Illuminate\Http\JsonResponse; |
14
|
|
|
use Illuminate\Http\Request; |
15
|
|
|
use Illuminate\Http\Response; |
16
|
|
|
use Illuminate\Support\Arr; |
17
|
|
|
|
18
|
|
|
class DataTables |
19
|
|
|
{ |
20
|
|
|
/** @var Request $request */ |
21
|
|
|
private $request; |
22
|
|
|
|
23
|
|
|
/** @var string $table */ |
24
|
|
|
private $table; |
25
|
|
|
|
26
|
|
|
/** @var array $tableColumns */ |
27
|
|
|
private $tableColumns; |
28
|
|
|
|
29
|
|
|
/** @var Model $model */ |
30
|
|
|
private $model; |
31
|
|
|
|
32
|
|
|
/** @var Builder $query */ |
33
|
|
|
private $query; |
34
|
|
|
|
35
|
|
|
/** @var Builder $query */ |
36
|
|
|
private $originalQuery; |
37
|
|
|
|
38
|
|
|
/** @var array|null $aliases */ |
39
|
|
|
private $aliases; |
40
|
|
|
|
41
|
|
|
/** @var DatabaseManager */ |
42
|
|
|
private $DB; |
43
|
|
|
|
44
|
|
|
public function __construct(Request $request, DatabaseManager $DB) |
45
|
|
|
{ |
46
|
|
|
$this->request = $request; |
47
|
|
|
$this->DB = $DB; |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @param \Illuminate\Database\Eloquent\Model $model |
52
|
|
|
* @param \Illuminate\Database\Eloquent\Builder $query |
53
|
|
|
* @param array $aliases |
54
|
|
|
* |
55
|
|
|
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse |
56
|
|
|
*/ |
57
|
|
|
public function provide(Model $model, Builder $query = null, array $aliases = null) |
58
|
|
|
{ |
59
|
|
|
if ($this->request->has(['draw', 'start', 'length'])) { |
60
|
|
|
$this->model = $model; |
61
|
|
|
$this->query = $query ?? $this->model->newQuery(); |
62
|
|
|
$this->aliases = $aliases; |
63
|
|
|
|
64
|
|
|
$this->table = $model->getTable(); |
65
|
|
|
$this->tableColumns = $this->DB |
66
|
|
|
->connection($model->getConnectionName()) |
67
|
|
|
->getDoctrineSchemaManager() |
68
|
|
|
->listTableColumns($this->table); |
69
|
|
|
|
70
|
|
|
$reqData = $this->request->all(); |
71
|
|
|
$response = []; |
72
|
|
|
|
73
|
|
|
$this->prepareSelects(); |
74
|
|
|
$this->originalQuery = clone $query; |
75
|
|
|
|
76
|
|
|
if (array_key_exists('columns', $reqData) && is_array($reqData['columns'])) { |
77
|
|
|
$columns = $reqData['columns']; |
78
|
|
|
|
79
|
|
|
if (is_array($reqData['columns'])) { |
80
|
|
|
$this->applySearch($columns); |
81
|
|
|
$this->applyOrder($reqData, $columns); |
82
|
|
|
} |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
$response['draw'] = +$reqData['draw']; |
86
|
|
|
$response = $this->setResultCounters($response); |
87
|
|
|
|
88
|
|
|
$this->applyPagination($reqData); |
89
|
|
|
|
90
|
|
|
$response['data'] = $this->query |
91
|
|
|
->get() |
92
|
|
|
->toArray(); |
93
|
|
|
|
94
|
|
|
return new JsonResponse($response); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
return new Response('', 400); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
private function prepareSelects() |
101
|
|
|
{ |
102
|
|
|
$tableAttr = array_keys( |
103
|
|
|
array_diff_key( |
104
|
|
|
array_flip( |
105
|
|
|
array_keys($this->tableColumns) |
106
|
|
|
), |
107
|
|
|
array_flip($this->model->getHidden()) |
108
|
|
|
) |
109
|
|
|
); |
110
|
|
|
|
111
|
|
|
if ($tableAttr) { |
|
|
|
|
112
|
|
|
foreach ($tableAttr as $attr) { |
113
|
|
|
$selects[] = $this->DB->raw($this->table.'.'.$attr); |
114
|
|
|
} |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
if ($this->aliases) { |
118
|
|
|
foreach ($this->aliases as $alias => $value) { |
119
|
|
|
$selects[] = $this->DB->raw($value.' AS '.$alias); |
120
|
|
|
} |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
if (isset($selects)) { |
124
|
|
|
$this->query->select($selects); |
125
|
|
|
} |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
private function applySearch(array $columns) |
129
|
|
|
{ |
130
|
|
|
foreach ($columns as $column) { |
131
|
|
|
$searchValue = Arr::get($column, 'search.value'); |
132
|
|
|
$searchColumn = Arr::get($column, 'data'); |
133
|
|
|
|
134
|
|
|
if (!is_null($searchValue) && !is_null($searchColumn)) { |
135
|
|
|
$searchField = $this->getField($searchColumn); |
136
|
|
|
if (!$searchField) { |
137
|
|
|
continue; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
$searchMethod = $this->getSearchMethod($searchField); |
141
|
|
|
[$searchQuery, $searchBindings] = $this->getSearchQuery($searchField, $searchValue, $searchColumn); |
142
|
|
|
|
143
|
|
|
$this->query->{$searchMethod}($searchQuery, $searchBindings); |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
private function getSearchQuery($searchField, $searchValue, $column) |
149
|
|
|
{ |
150
|
|
|
if ($this->isDateRange($searchValue)) { |
151
|
|
|
[$from, $to] = explode(' - ', $searchValue); |
152
|
|
|
|
153
|
|
|
$from = $this->toMySQLDate($from); |
154
|
|
|
$to = $this->toMySQLDate($to, 1); |
155
|
|
|
|
156
|
|
|
return [ |
157
|
|
|
$searchField.' between ? and ?', |
158
|
|
|
[$from, $to], |
159
|
|
|
]; |
160
|
|
|
} else { |
161
|
|
|
if ($this->shouldUseLike($this->tableColumns, $column)) { |
162
|
|
|
return [ |
163
|
|
|
$searchField.' like ?', |
164
|
|
|
['%'.$searchValue.'%'], |
165
|
|
|
]; |
166
|
|
|
} else { |
167
|
|
|
return [ |
168
|
|
|
$searchField.' = ?', |
169
|
|
|
[$searchValue], |
170
|
|
|
]; |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
private function isDateRange($value) : bool |
176
|
|
|
{ |
177
|
|
|
return (bool) (strlen($value) === 23) && |
178
|
|
|
preg_match('^\\d{2}/\\d{2}/\\d{4} - \\d{2}/\\d{2}/\\d{4}^', $value); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
private function toMySQLDate($value, $plusDay = 0) |
182
|
|
|
{ |
183
|
|
|
return Carbon::createFromFormat('d/m/Y', $value) |
184
|
|
|
->addDays($plusDay) |
185
|
|
|
->toDateString(); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
private function applyOrder(array $reqData, array $columns) |
189
|
|
|
{ |
190
|
|
|
if (array_key_exists('order', $reqData)) { |
191
|
|
|
$orderColumnId = Arr::get($reqData, 'order.0.column'); |
192
|
|
|
$orderByColumn = Arr::get($columns, $orderColumnId.'.data'); |
193
|
|
|
|
194
|
|
|
if (!is_null($orderColumnId) && !is_null($orderByColumn)) { |
195
|
|
|
$direction = Arr::get($reqData, 'order.0.dir'); |
196
|
|
|
if ($direction !== 'asc' && $direction !== 'desc') { |
197
|
|
|
return; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
$orderField = $this->getField($orderByColumn); |
201
|
|
|
if (!$orderField) { |
202
|
|
|
return; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
$this->query->orderByRaw($orderField.' '.$direction); |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
private function getField($column) |
211
|
|
|
{ |
212
|
|
|
if (empty($this->aliases) || !array_key_exists($column, $this->aliases)) { |
213
|
|
|
if (array_key_exists($column, $this->tableColumns)) { |
214
|
|
|
return $this->table.'.'.$column; |
215
|
|
|
} else { |
216
|
|
|
return null; |
217
|
|
|
} |
218
|
|
|
} else { |
219
|
|
|
return $this->aliases[$column]; |
220
|
|
|
} |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
private function setResultCounters(array $response) : array |
224
|
|
|
{ |
225
|
|
|
$response['recordsTotal'] = $this->getCount($this->originalQuery); |
226
|
|
|
|
227
|
|
|
if (!empty($this->query->getQuery()->wheres) && |
228
|
|
|
$this->originalQuery->getQuery()->wheres !== $this->query->getQuery()->wheres) { |
229
|
|
|
$response['recordsFiltered'] = $this->getCount($this->query); |
230
|
|
|
} else { |
231
|
|
|
$response['recordsFiltered'] = $response['recordsTotal']; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
return $response; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
private function getCount(Builder $query) : int |
238
|
|
|
{ |
239
|
|
|
if (!empty($query->getQuery()->groups) || !empty($query->getQuery()->havings)) { |
240
|
|
|
return $this->DB |
241
|
|
|
->table($this->DB->raw('('.$query->toSql().') as s')) |
|
|
|
|
242
|
|
|
->setBindings($query->getBindings()) |
|
|
|
|
243
|
|
|
->selectRaw('count(*) as count') |
244
|
|
|
->first() |
245
|
|
|
->count; |
246
|
|
|
} else { |
247
|
|
|
return $query->count(); |
|
|
|
|
248
|
|
|
} |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
private function applyPagination(array $reqData) |
252
|
|
|
{ |
253
|
|
|
if (array_key_exists('start', $reqData)) { |
254
|
|
|
$this->query->offset(+$reqData['start']); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
if (array_key_exists('length', $reqData)) { |
258
|
|
|
$this->query->limit(+$reqData['length']); |
259
|
|
|
} |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
private function getSearchMethod($alias) |
263
|
|
|
{ |
264
|
|
|
$mustUseHaving = ['GROUP_CONCAT', 'COUNT', 'MIN', 'IFNULL']; |
265
|
|
|
|
266
|
|
|
foreach ($mustUseHaving as $m) { |
267
|
|
|
if (strpos($alias, $m) !== false) { |
268
|
|
|
return 'havingRaw'; |
269
|
|
|
} |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
return 'whereRaw'; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* @param Column[] $tableColumns |
277
|
|
|
* @param string $column |
278
|
|
|
* |
279
|
|
|
* @return mixed |
280
|
|
|
*/ |
281
|
|
|
private function shouldUseLike($tableColumns, $column) |
282
|
|
|
{ |
283
|
|
|
if (!array_key_exists($column, $tableColumns)) { |
284
|
|
|
return true; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
return !($tableColumns[$column]->getType() instanceof IntegerType || |
288
|
|
|
$tableColumns[$column]->getType() instanceof SmallIntType || |
289
|
|
|
$tableColumns[$column]->getType() instanceof BigIntType); |
290
|
|
|
} |
291
|
|
|
} |
292
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.