These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace Yajra\Datatables\Engines; |
||
4 | |||
5 | use Closure; |
||
6 | use Illuminate\Database\Eloquent\Relations\BelongsToMany; |
||
7 | use Illuminate\Database\Eloquent\Relations\HasOne; |
||
8 | use Illuminate\Database\Eloquent\Relations\HasMany; |
||
9 | use Illuminate\Database\Query\Builder; |
||
10 | use Illuminate\Support\Str; |
||
11 | use Yajra\Datatables\Helper; |
||
12 | use Yajra\Datatables\Request; |
||
13 | |||
14 | /** |
||
15 | * Class QueryBuilderEngine. |
||
16 | * |
||
17 | * @package Yajra\Datatables\Engines |
||
18 | * @author Arjay Angeles <[email protected]> |
||
19 | */ |
||
20 | class QueryBuilderEngine extends BaseEngine |
||
21 | { |
||
22 | /** |
||
23 | * @param \Illuminate\Database\Query\Builder $builder |
||
24 | * @param \Yajra\Datatables\Request $request |
||
25 | */ |
||
26 | public function __construct(Builder $builder, Request $request) |
||
27 | { |
||
28 | $this->query = $builder; |
||
29 | $this->init($request, $builder); |
||
30 | } |
||
31 | |||
32 | /** |
||
33 | * Initialize attributes. |
||
34 | * |
||
35 | * @param \Yajra\Datatables\Request $request |
||
36 | * @param \Illuminate\Database\Query\Builder $builder |
||
37 | * @param string $type |
||
38 | */ |
||
39 | protected function init($request, $builder, $type = 'builder') |
||
40 | { |
||
41 | $this->request = $request; |
||
42 | $this->query_type = $type; |
||
43 | $this->columns = $builder->columns; |
||
44 | $this->connection = $builder->getConnection(); |
||
45 | $this->prefix = $this->connection->getTablePrefix(); |
||
46 | $this->database = $this->connection->getDriverName(); |
||
47 | if ($this->isDebugging()) { |
||
48 | $this->connection->enableQueryLog(); |
||
49 | } |
||
50 | } |
||
51 | |||
52 | /** |
||
53 | * Set auto filter off and run your own filter. |
||
54 | * Overrides global search |
||
55 | * |
||
56 | * @param \Closure $callback |
||
57 | * @param bool $globalSearch |
||
58 | * @return $this |
||
59 | */ |
||
60 | public function filter(Closure $callback, $globalSearch = false) |
||
61 | { |
||
62 | $this->overrideGlobalSearch($callback, $this->query, $globalSearch); |
||
63 | |||
64 | return $this; |
||
65 | } |
||
66 | |||
67 | /** |
||
68 | * Organizes works |
||
69 | * |
||
70 | * @param bool $mDataSupport |
||
71 | * @param bool $orderFirst |
||
72 | * @return \Illuminate\Http\JsonResponse |
||
73 | */ |
||
74 | public function make($mDataSupport = false, $orderFirst = false) |
||
75 | { |
||
76 | return parent::make($mDataSupport, $orderFirst); |
||
77 | } |
||
78 | |||
79 | /** |
||
80 | * Count total items. |
||
81 | * |
||
82 | * @return integer |
||
83 | */ |
||
84 | public function totalCount() |
||
85 | { |
||
86 | return $this->totalRecords ? $this->totalRecords : $this->count(); |
||
87 | } |
||
88 | |||
89 | /** |
||
90 | * Counts current query. |
||
91 | * |
||
92 | * @return int |
||
93 | */ |
||
94 | public function count() |
||
95 | { |
||
96 | $myQuery = clone $this->query; |
||
97 | // if its a normal query ( no union, having and distinct word ) |
||
98 | // replace the select with static text to improve performance |
||
99 | if (! Str::contains(Str::lower($myQuery->toSql()), ['union', 'having', 'distinct', 'order by', 'group by'])) { |
||
100 | $row_count = $this->wrap('row_count'); |
||
101 | $myQuery->select($this->connection->raw("'1' as {$row_count}")); |
||
102 | } |
||
103 | |||
104 | return $this->connection->table($this->connection->raw('(' . $myQuery->toSql() . ') count_row_table')) |
||
105 | ->setBindings($myQuery->getBindings())->count(); |
||
106 | } |
||
107 | |||
108 | /** |
||
109 | * Wrap column with DB grammar. |
||
110 | * |
||
111 | * @param string $column |
||
112 | * @return string |
||
113 | */ |
||
114 | protected function wrap($column) |
||
115 | { |
||
116 | return $this->connection->getQueryGrammar()->wrap($column); |
||
117 | } |
||
118 | |||
119 | /** |
||
120 | * Perform global search. |
||
121 | * |
||
122 | * @return void |
||
123 | */ |
||
124 | public function filtering() |
||
125 | { |
||
126 | $this->query->where( |
||
127 | function ($query) { |
||
128 | $globalKeyword = $this->request->keyword(); |
||
129 | $queryBuilder = $this->getQueryBuilder($query); |
||
130 | |||
131 | foreach ($this->request->searchableColumnIndex() as $index) { |
||
132 | $columnName = $this->getColumnName($index); |
||
133 | if ($this->isBlacklisted($columnName)) { |
||
134 | continue; |
||
135 | } |
||
136 | |||
137 | // check if custom column filtering is applied |
||
138 | if (isset($this->columnDef['filter'][$columnName])) { |
||
139 | $columnDef = $this->columnDef['filter'][$columnName]; |
||
140 | // check if global search should be applied for the specific column |
||
141 | $applyGlobalSearch = count($columnDef['parameters']) == 0 || end($columnDef['parameters']) !== false; |
||
142 | if (! $applyGlobalSearch) { |
||
143 | continue; |
||
144 | } |
||
145 | |||
146 | View Code Duplication | if ($columnDef['method'] instanceof Closure) { |
|
147 | $whereQuery = $queryBuilder->newQuery(); |
||
148 | call_user_func_array($columnDef['method'], [$whereQuery, $globalKeyword]); |
||
149 | $queryBuilder->addNestedWhereQuery($whereQuery, 'or'); |
||
150 | } else { |
||
151 | $this->compileColumnQuery( |
||
152 | $queryBuilder, |
||
153 | Helper::getOrMethod($columnDef['method']), |
||
154 | $columnDef['parameters'], |
||
155 | $columnName, |
||
156 | $globalKeyword |
||
157 | ); |
||
158 | } |
||
159 | View Code Duplication | } else { |
|
0 ignored issues
–
show
|
|||
160 | if (count(explode('.', $columnName)) > 1) { |
||
161 | $eagerLoads = $this->getEagerLoads(); |
||
162 | $parts = explode('.', $columnName); |
||
163 | $relationColumn = array_pop($parts); |
||
164 | $relation = implode('.', $parts); |
||
165 | if (in_array($relation, $eagerLoads)) { |
||
166 | $this->compileRelationSearch( |
||
167 | $queryBuilder, |
||
168 | $relation, |
||
169 | $relationColumn, |
||
170 | $globalKeyword |
||
171 | ); |
||
172 | } else { |
||
173 | $this->compileQuerySearch($queryBuilder, $columnName, $globalKeyword); |
||
174 | } |
||
175 | } else { |
||
176 | $this->compileQuerySearch($queryBuilder, $columnName, $globalKeyword); |
||
177 | } |
||
178 | } |
||
179 | |||
180 | $this->isFilterApplied = true; |
||
181 | } |
||
182 | } |
||
183 | ); |
||
184 | } |
||
185 | |||
186 | /** |
||
187 | * Perform filter column on selected field. |
||
188 | * |
||
189 | * @param mixed $query |
||
190 | * @param string|Closure $method |
||
191 | * @param mixed $parameters |
||
192 | * @param string $column |
||
193 | * @param string $keyword |
||
194 | */ |
||
195 | protected function compileColumnQuery($query, $method, $parameters, $column, $keyword) |
||
196 | { |
||
197 | if (method_exists($query, $method) |
||
198 | && count($parameters) <= with(new \ReflectionMethod($query, $method))->getNumberOfParameters() |
||
199 | ) { |
||
200 | if (Str::contains(Str::lower($method), 'raw') |
||
201 | || Str::contains(Str::lower($method), 'exists') |
||
202 | ) { |
||
203 | call_user_func_array( |
||
204 | [$query, $method], |
||
205 | $this->parameterize($parameters, $keyword) |
||
206 | ); |
||
207 | } else { |
||
208 | call_user_func_array( |
||
209 | [$query, $method], |
||
210 | $this->parameterize($column, $parameters, $keyword) |
||
211 | ); |
||
212 | } |
||
213 | } |
||
214 | } |
||
215 | |||
216 | /** |
||
217 | * Build Query Builder Parameters. |
||
218 | * |
||
219 | * @return array |
||
220 | */ |
||
221 | protected function parameterize() |
||
222 | { |
||
223 | $args = func_get_args(); |
||
224 | $keyword = count($args) > 2 ? $args[2] : $args[1]; |
||
225 | $parameters = Helper::buildParameters($args); |
||
226 | $parameters = Helper::replacePatternWithKeyword($parameters, $keyword, '$1'); |
||
227 | |||
228 | return $parameters; |
||
229 | } |
||
230 | |||
231 | /** |
||
232 | * Get eager loads keys if eloquent. |
||
233 | * |
||
234 | * @return array |
||
235 | */ |
||
236 | protected function getEagerLoads() |
||
237 | { |
||
238 | if ($this->query_type == 'eloquent') { |
||
239 | return array_keys($this->query->getEagerLoads()); |
||
240 | } |
||
241 | |||
242 | return []; |
||
243 | } |
||
244 | |||
245 | /** |
||
246 | * Add relation query on global search. |
||
247 | * |
||
248 | * @param mixed $query |
||
249 | * @param string $relation |
||
250 | * @param string $column |
||
251 | * @param string $keyword |
||
252 | */ |
||
253 | protected function compileRelationSearch($query, $relation, $column, $keyword) |
||
254 | { |
||
255 | $myQuery = clone $this->query; |
||
256 | $myQuery->orWhereHas($relation, function ($builder) use ($column, $keyword, $query) { |
||
257 | $builder->select($this->connection->raw('count(1)')); |
||
258 | $this->compileQuerySearch($builder, $column, $keyword, ''); |
||
259 | $builder = "({$builder->toSql()}) >= 1"; |
||
260 | |||
261 | $query->orWhereRaw($builder, [$this->prepareKeyword($keyword)]); |
||
262 | }); |
||
263 | } |
||
264 | |||
265 | /** |
||
266 | * Compile query builder where clause depending on configurations. |
||
267 | * |
||
268 | * @param mixed $query |
||
269 | * @param string $column |
||
270 | * @param string $keyword |
||
271 | * @param string $relation |
||
272 | */ |
||
273 | protected function compileQuerySearch($query, $column, $keyword, $relation = 'or') |
||
274 | { |
||
275 | $column = strstr($column, '(') ? $this->connection->raw($column) : $column; |
||
276 | $column = $this->castColumn($column); |
||
277 | $sql = $column . ' LIKE ?'; |
||
278 | |||
279 | if ($this->isCaseInsensitive()) { |
||
280 | $sql = 'LOWER(' . $column . ') LIKE ?'; |
||
281 | } |
||
282 | |||
283 | $query->{$relation . 'WhereRaw'}($sql, [$this->prepareKeyword($keyword)]); |
||
284 | } |
||
285 | |||
286 | /** |
||
287 | * Wrap a column and cast in pgsql. |
||
288 | * |
||
289 | * @param string $column |
||
290 | * @return string |
||
291 | */ |
||
292 | public function castColumn($column) |
||
293 | { |
||
294 | $column = $this->wrap($column); |
||
295 | if ($this->database === 'pgsql') { |
||
296 | $column = 'CAST(' . $column . ' as TEXT)'; |
||
297 | } elseif ($this->database === 'firebird') { |
||
298 | $column = 'CAST(' . $column . ' as VARCHAR(255))'; |
||
299 | } |
||
300 | |||
301 | return $column; |
||
302 | } |
||
303 | |||
304 | /** |
||
305 | * Prepare search keyword based on configurations. |
||
306 | * |
||
307 | * @param string $keyword |
||
308 | * @return string |
||
309 | */ |
||
310 | protected function prepareKeyword($keyword) |
||
311 | { |
||
312 | if ($this->isCaseInsensitive()) { |
||
313 | $keyword = Str::lower($keyword); |
||
314 | } |
||
315 | |||
316 | if ($this->isWildcard()) { |
||
317 | $keyword = $this->wildcardLikeString($keyword); |
||
318 | } |
||
319 | |||
320 | if ($this->isSmartSearch()) { |
||
321 | $keyword = "%$keyword%"; |
||
322 | } |
||
323 | |||
324 | return $keyword; |
||
325 | } |
||
326 | |||
327 | /** |
||
328 | * Perform column search. |
||
329 | * |
||
330 | * @return void |
||
331 | */ |
||
332 | public function columnSearch() |
||
333 | { |
||
334 | $columns = $this->request->get('columns', []); |
||
335 | |||
336 | foreach ($columns as $index => $column) { |
||
337 | if (! $this->request->isColumnSearchable($index)) { |
||
338 | continue; |
||
339 | } |
||
340 | |||
341 | $column = $this->getColumnName($index); |
||
342 | |||
343 | if (isset($this->columnDef['filter'][$column])) { |
||
344 | $columnDef = $this->columnDef['filter'][$column]; |
||
345 | // get a raw keyword (without wildcards) |
||
346 | $keyword = $this->getSearchKeyword($index, true); |
||
347 | $builder = $this->getQueryBuilder(); |
||
348 | |||
349 | View Code Duplication | if ($columnDef['method'] instanceof Closure) { |
|
350 | $whereQuery = $builder->newQuery(); |
||
351 | call_user_func_array($columnDef['method'], [$whereQuery, $keyword]); |
||
352 | $builder->addNestedWhereQuery($whereQuery); |
||
353 | } else { |
||
354 | $this->compileColumnQuery( |
||
355 | $builder, |
||
356 | $columnDef['method'], |
||
357 | $columnDef['parameters'], |
||
358 | $column, |
||
359 | $keyword |
||
360 | ); |
||
361 | } |
||
362 | View Code Duplication | } else { |
|
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository.
Loading history...
|
|||
363 | if (count(explode('.', $column)) > 1) { |
||
364 | $eagerLoads = $this->getEagerLoads(); |
||
365 | $parts = explode('.', $column); |
||
366 | $relationColumn = array_pop($parts); |
||
367 | $relation = implode('.', $parts); |
||
368 | if (in_array($relation, $eagerLoads)) { |
||
369 | $column = $this->joinEagerLoadedColumn($relation, $relationColumn); |
||
370 | } |
||
371 | } |
||
372 | |||
373 | $keyword = $this->getSearchKeyword($index); |
||
374 | $this->compileColumnSearch($index, $column, $keyword); |
||
375 | } |
||
376 | |||
377 | $this->isFilterApplied = true; |
||
378 | } |
||
379 | } |
||
380 | |||
381 | /** |
||
382 | * Get proper keyword to use for search. |
||
383 | * |
||
384 | * @param int $i |
||
385 | * @param bool $raw |
||
386 | * @return string |
||
387 | */ |
||
388 | private function getSearchKeyword($i, $raw = false) |
||
389 | { |
||
390 | $keyword = $this->request->columnKeyword($i); |
||
391 | if ($raw || $this->request->isRegex($i)) { |
||
392 | return $keyword; |
||
393 | } |
||
394 | |||
395 | return $this->setupKeyword($keyword); |
||
396 | } |
||
397 | |||
398 | /** |
||
399 | * Join eager loaded relation and get the related column name. |
||
400 | * |
||
401 | * @param string $relation |
||
402 | * @param string $relationColumn |
||
403 | * @return string |
||
404 | */ |
||
405 | protected function joinEagerLoadedColumn($relation, $relationColumn) |
||
406 | { |
||
407 | $joins = []; |
||
408 | foreach ((array) $this->getQueryBuilder()->joins as $key => $join) { |
||
409 | $joins[] = $join->table; |
||
410 | } |
||
411 | |||
412 | $model = $this->query->getRelation($relation); |
||
413 | if ($model instanceof BelongsToMany) { |
||
414 | $pivot = $model->getTable(); |
||
415 | $pivotPK = $model->getForeignKey(); |
||
416 | $pivotFK = $model->getQualifiedParentKeyName(); |
||
417 | |||
418 | if (! in_array($pivot, $joins)) { |
||
419 | $this->getQueryBuilder()->leftJoin($pivot, $pivotPK, '=', $pivotFK); |
||
420 | } |
||
421 | |||
422 | $related = $model->getRelated(); |
||
423 | $table = $related->getTable(); |
||
424 | $tablePK = $related->getForeignKey(); |
||
425 | $tableFK = $related->getQualifiedKeyName(); |
||
426 | |||
427 | if (! in_array($table, $joins)) { |
||
428 | $this->getQueryBuilder()->leftJoin($table, $pivot . '.' . $tablePK, '=', $tableFK); |
||
429 | } |
||
430 | } else { |
||
431 | $table = $model->getRelated()->getTable(); |
||
432 | if ($model instanceof HasOne || $model instanceof HasMany) { |
||
433 | $foreign = $model->getForeignKey(); |
||
434 | $other = $model->getQualifiedParentKeyName(); |
||
435 | } else { |
||
436 | $foreign = $model->getQualifiedForeignKey(); |
||
437 | $other = $model->getQualifiedOtherKeyName(); |
||
438 | } |
||
439 | |||
440 | if (! in_array($table, $joins)) { |
||
441 | $this->getQueryBuilder()->leftJoin($table, $foreign, '=', $other); |
||
442 | } |
||
443 | } |
||
444 | |||
445 | $column = $table . '.' . $relationColumn; |
||
446 | |||
447 | return $column; |
||
448 | } |
||
449 | |||
450 | /** |
||
451 | * Compile queries for column search. |
||
452 | * |
||
453 | * @param int $i |
||
454 | * @param mixed $column |
||
455 | * @param string $keyword |
||
456 | */ |
||
457 | protected function compileColumnSearch($i, $column, $keyword) |
||
458 | { |
||
459 | if ($this->request->isRegex($i)) { |
||
460 | $column = strstr($column, '(') ? $this->connection->raw($column) : $column; |
||
461 | $this->regexColumnSearch($column, $keyword); |
||
462 | } else { |
||
463 | $this->compileQuerySearch($this->query, $column, $keyword, ''); |
||
464 | } |
||
465 | } |
||
466 | |||
467 | /** |
||
468 | * Compile regex query column search. |
||
469 | * |
||
470 | * @param mixed $column |
||
471 | * @param string $keyword |
||
472 | */ |
||
473 | protected function regexColumnSearch($column, $keyword) |
||
474 | { |
||
475 | if ($this->isOracleSql()) { |
||
476 | $sql = ! $this->isCaseInsensitive() ? 'REGEXP_LIKE( ' . $column . ' , ? )' : 'REGEXP_LIKE( LOWER(' . $column . ') , ?, \'i\' )'; |
||
477 | $this->query->whereRaw($sql, [$keyword]); |
||
478 | } else { |
||
479 | $sql = ! $this->isCaseInsensitive() ? $column . ' REGEXP ?' : 'LOWER(' . $column . ') REGEXP ?'; |
||
480 | $this->query->whereRaw($sql, [Str::lower($keyword)]); |
||
481 | } |
||
482 | } |
||
483 | |||
484 | /** |
||
485 | * Perform sorting of columns. |
||
486 | * |
||
487 | * @return void |
||
488 | */ |
||
489 | public function ordering() |
||
490 | { |
||
491 | if ($this->orderCallback) { |
||
492 | call_user_func($this->orderCallback, $this->getQueryBuilder()); |
||
493 | |||
494 | return; |
||
495 | } |
||
496 | |||
497 | foreach ($this->request->orderableColumns() as $orderable) { |
||
498 | $column = $this->getColumnName($orderable['column'], true); |
||
499 | |||
500 | if ($this->isBlacklisted($column)) { |
||
501 | continue; |
||
502 | } |
||
503 | |||
504 | if (isset($this->columnDef['order'][$column])) { |
||
505 | $method = $this->columnDef['order'][$column]['method']; |
||
506 | $parameters = $this->columnDef['order'][$column]['parameters']; |
||
507 | $this->compileColumnQuery( |
||
508 | $this->getQueryBuilder(), |
||
509 | $method, |
||
510 | $parameters, |
||
511 | $column, |
||
512 | $orderable['direction'] |
||
513 | ); |
||
514 | } else { |
||
515 | if (count(explode('.', $column)) > 1) { |
||
516 | $eagerLoads = $this->getEagerLoads(); |
||
517 | $parts = explode('.', $column); |
||
518 | $relationColumn = array_pop($parts); |
||
519 | $relation = implode('.', $parts); |
||
520 | |||
521 | if (in_array($relation, $eagerLoads)) { |
||
522 | $column = $this->joinEagerLoadedColumn($relation, $relationColumn); |
||
523 | } |
||
524 | } |
||
525 | |||
526 | $this->getQueryBuilder()->orderBy($column, $orderable['direction']); |
||
527 | } |
||
528 | } |
||
529 | } |
||
530 | |||
531 | /** |
||
532 | * Perform pagination |
||
533 | * |
||
534 | * @return void |
||
535 | */ |
||
536 | public function paging() |
||
537 | { |
||
538 | $this->query->skip($this->request['start']) |
||
539 | ->take((int) $this->request['length'] > 0 ? $this->request['length'] : 10); |
||
540 | } |
||
541 | |||
542 | /** |
||
543 | * Get results |
||
544 | * |
||
545 | * @return array|static[] |
||
546 | */ |
||
547 | public function results() |
||
548 | { |
||
549 | return $this->query->get(); |
||
550 | } |
||
551 | } |
||
552 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.