Completed
Push — master ( 818c2f...233141 )
by Arjay
05:20
created

src/Engines/QueryBuilderEngine.php (2 issues)

Upgrade to new PHP Analysis Engine

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
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...
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