Completed
Push — master ( 25b0dd...4e0abf )
by Arjay
02:10
created

src/Engines/QueryBuilderEngine.php (1 issue)

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\HasOneOrMany;
8
use Illuminate\Database\Eloquent\Relations\MorphToMany;
9
use Illuminate\Database\Query\Builder;
10
use Illuminate\Database\Query\Expression;
11
use Illuminate\Support\Facades\Config;
12
use Illuminate\Support\Str;
13
use Yajra\Datatables\Helper;
14
use Yajra\Datatables\Request;
15
16
/**
17
 * Class QueryBuilderEngine.
18
 *
19
 * @package Yajra\Datatables\Engines
20
 * @author  Arjay Angeles <[email protected]>
21
 */
22
class QueryBuilderEngine extends BaseEngine
23
{
24
    /**
25
     * @param \Illuminate\Database\Query\Builder $builder
26
     * @param \Yajra\Datatables\Request $request
27
     */
28
    public function __construct(Builder $builder, Request $request)
29
    {
30
        $this->query = $builder;
31
        $this->init($request, $builder);
32
    }
33
34
    /**
35
     * Initialize attributes.
36
     *
37
     * @param  \Yajra\Datatables\Request $request
38
     * @param  \Illuminate\Database\Query\Builder $builder
39
     * @param  string $type
40
     */
41
    protected function init($request, $builder, $type = 'builder')
42
    {
43
        $this->request    = $request;
44
        $this->query_type = $type;
45
        $this->columns    = $builder->columns;
46
        $this->connection = $builder->getConnection();
47
        $this->prefix     = $this->connection->getTablePrefix();
48
        $this->database   = $this->connection->getDriverName();
49
        if ($this->isDebugging()) {
50
            $this->connection->enableQueryLog();
51
        }
52
    }
53
54
    /**
55
     * Set auto filter off and run your own filter.
56
     * Overrides global search
57
     *
58
     * @param \Closure $callback
59
     * @param bool $globalSearch
60
     * @return $this
61
     */
62
    public function filter(Closure $callback, $globalSearch = false)
63
    {
64
        $this->overrideGlobalSearch($callback, $this->query, $globalSearch);
65
66
        return $this;
67
    }
68
69
    /**
70
     * Organizes works
71
     *
72
     * @param bool $mDataSupport
73
     * @param bool $orderFirst
74
     * @return \Illuminate\Http\JsonResponse
75
     */
76
    public function make($mDataSupport = false, $orderFirst = false)
77
    {
78
        return parent::make($mDataSupport, $orderFirst);
79
    }
80
81
    /**
82
     * Count total items.
83
     *
84
     * @return integer
85
     */
86
    public function totalCount()
87
    {
88
        return $this->totalRecords ? $this->totalRecords : $this->count();
89
    }
90
91
    /**
92
     * Counts current query.
93
     *
94
     * @return int
95
     */
96
    public function count()
97
    {
98
        $myQuery = clone $this->query;
99
        // if its a normal query ( no union, having and distinct word )
100
        // replace the select with static text to improve performance
101
        if (! Str::contains(Str::lower($myQuery->toSql()), ['union', 'having', 'distinct', 'order by', 'group by'])) {
102
            $row_count = $this->wrap('row_count');
103
            $myQuery->select($this->connection->raw("'1' as {$row_count}"));
104
        }
105
106
        // check for select soft deleted records
107
        if (! $this->withTrashed && $this->modelUseSoftDeletes()) {
108
            $myQuery->whereNull($myQuery->getModel()->getQualifiedDeletedAtColumn());
109
        }
110
111
        return $this->connection->table($this->connection->raw('(' . $myQuery->toSql() . ') count_row_table'))
112
                                ->setBindings($myQuery->getBindings())->count();
113
    }
114
115
    /**
116
     * Wrap column with DB grammar.
117
     *
118
     * @param string $column
119
     * @return string
120
     */
121
    protected function wrap($column)
122
    {
123
        return $this->connection->getQueryGrammar()->wrap($column);
124
    }
125
126
    /**
127
     * Check if model use SoftDeletes trait
128
     *
129
     * @return boolean
130
     */
131
    private function modelUseSoftDeletes()
132
    {
133
        if ($this->query_type == 'eloquent') {
134
            return in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses($this->query->getModel()));
135
        }
136
137
        return false;
138
    }
139
140
    /**
141
     * Perform global search.
142
     *
143
     * @return void
144
     */
145
    public function filtering()
146
    {
147
        $this->query->where(
148
            function ($query) {
149
                $globalKeyword = $this->request->keyword();
150
                $queryBuilder  = $this->getQueryBuilder($query);
151
152
                foreach ($this->request->searchableColumnIndex() as $index) {
153
                    $columnName = $this->getColumnName($index);
154
                    if ($this->isBlacklisted($columnName)) {
155
                        continue;
156
                    }
157
158
                    // check if custom column filtering is applied
159
                    if (isset($this->columnDef['filter'][$columnName])) {
160
                        $columnDef = $this->columnDef['filter'][$columnName];
161
                        // check if global search should be applied for the specific column
162
                        $applyGlobalSearch = count($columnDef['parameters']) == 0 || end($columnDef['parameters']) !== false;
163
                        if (! $applyGlobalSearch) {
164
                            continue;
165
                        }
166
167 View Code Duplication
                        if ($columnDef['method'] instanceof Closure) {
168
                            $whereQuery = $queryBuilder->newQuery();
169
                            call_user_func_array($columnDef['method'], [$whereQuery, $globalKeyword]);
170
                            $queryBuilder->addNestedWhereQuery($whereQuery, 'or');
171
                        } else {
172
                            $this->compileColumnQuery(
173
                                $queryBuilder,
174
                                Helper::getOrMethod($columnDef['method']),
175
                                $columnDef['parameters'],
176
                                $columnName,
177
                                $globalKeyword
178
                            );
179
                        }
180 View Code Duplication
                    } else {
181
                        if (count(explode('.', $columnName)) > 1) {
182
                            $eagerLoads     = $this->getEagerLoads();
183
                            $parts          = explode('.', $columnName);
184
                            $relationColumn = array_pop($parts);
185
                            $relation       = implode('.', $parts);
186
                            if (in_array($relation, $eagerLoads)) {
187
                                $this->compileRelationSearch(
188
                                    $queryBuilder,
189
                                    $relation,
190
                                    $relationColumn,
191
                                    $globalKeyword
192
                                );
193
                            } else {
194
                                $this->compileQuerySearch($queryBuilder, $columnName, $globalKeyword);
195
                            }
196
                        } else {
197
                            $this->compileQuerySearch($queryBuilder, $columnName, $globalKeyword);
198
                        }
199
                    }
200
201
                    $this->isFilterApplied = true;
202
                }
203
            }
204
        );
205
    }
206
207
    /**
208
     * Perform filter column on selected field.
209
     *
210
     * @param mixed $query
211
     * @param string|Closure $method
212
     * @param mixed $parameters
213
     * @param string $column
214
     * @param string $keyword
215
     */
216
    protected function compileColumnQuery($query, $method, $parameters, $column, $keyword)
217
    {
218
        if (method_exists($query, $method)
219
            && count($parameters) <= with(new \ReflectionMethod($query, $method))->getNumberOfParameters()
220
        ) {
221
            if (Str::contains(Str::lower($method), 'raw')
222
                || Str::contains(Str::lower($method), 'exists')
223
            ) {
224
                call_user_func_array(
225
                    [$query, $method],
226
                    $this->parameterize($parameters, $keyword)
227
                );
228
            } else {
229
                call_user_func_array(
230
                    [$query, $method],
231
                    $this->parameterize($column, $parameters, $keyword)
232
                );
233
            }
234
        }
235
    }
236
237
    /**
238
     * Build Query Builder Parameters.
239
     *
240
     * @return array
241
     */
242
    protected function parameterize()
243
    {
244
        $args       = func_get_args();
245
        $keyword    = count($args) > 2 ? $args[2] : $args[1];
246
        $parameters = Helper::buildParameters($args);
247
        $parameters = Helper::replacePatternWithKeyword($parameters, $keyword, '$1');
248
249
        return $parameters;
250
    }
251
252
    /**
253
     * Get eager loads keys if eloquent.
254
     *
255
     * @return array
256
     */
257
    protected function getEagerLoads()
258
    {
259
        if ($this->query_type == 'eloquent') {
260
            return array_keys($this->query->getEagerLoads());
261
        }
262
263
        return [];
264
    }
265
266
    /**
267
     * Add relation query on global search.
268
     *
269
     * @param mixed $query
270
     * @param string $relation
271
     * @param string $column
272
     * @param string $keyword
273
     */
274
    protected function compileRelationSearch($query, $relation, $column, $keyword)
275
    {
276
        $myQuery = clone $this->query;
277
278
        /**
279
         * For compile nested relation, we need store all nested relation as array
280
         * and reverse order to apply where query.
281
         * With this method we can create nested sub query with properly relation.
282
         */
283
284
        /**
285
         * Store all relation data that require in next step
286
         */
287
        $relationChunk = [];
288
289
        /**
290
         * Store last eloquent query builder for get next relation.
291
         */
292
        $lastQuery = $query;
293
294
        $relations    = explode('.', $relation);
295
        $lastRelation = end($relations);
296
        foreach ($relations as $relation) {
297
            $relationType = $myQuery->getModel()->{$relation}();
298
            $myQuery->orWhereHas($relation, function ($builder) use (
299
                $column,
300
                $keyword,
301
                $query,
302
                $relationType,
303
                $relation,
304
                $lastRelation,
305
                &$relationChunk,
306
                &$lastQuery
307
            ) {
308
                $builder->select($this->connection->raw('count(1)'));
309
310
                // We will perform search on last relation only.
311
                if ($relation == $lastRelation) {
312
                    $this->compileQuerySearch($builder, $column, $keyword, '');
313
                }
314
315
                // Put require object to next step!!
316
                $relationChunk[$relation] = [
317
                    'builder'      => $builder,
318
                    'relationType' => $relationType,
319
                    'query'        => $lastQuery,
320
                ];
321
322
                // This is trick make sub query.
323
                $lastQuery = $builder;
324
            });
325
326
            // This is trick to make nested relation by pass previous relation to be next query eloquent builder
327
            $myQuery = $relationType;
328
        }
329
330
        /**
331
         * Reverse them all
332
         */
333
        $relationChunk = array_reverse($relationChunk, true);
334
335
        /**
336
         * Create valuable for use in check last relation
337
         */
338
        end($relationChunk);
339
        $lastRelation = key($relationChunk);
340
        reset($relationChunk);
341
342
        /**
343
         * Walking ...
344
         */
345
        foreach ($relationChunk as $relation => $chunk) {
346
            // Prepare variables
347
            $builder      = $chunk['builder'];
348
            $relationType = $chunk['relationType'];
349
            $query        = $chunk['query'];
350
            $builder      = "({$builder->toSql()}) >= 1";
351
352
            // Check if it last relation we will use orWhereRaw
353
            if ($lastRelation == $relation) {
354
                $relationMethod = "orWhereRaw";
355
            } else {
356
                // For case parent relation of nested relation.
357
                // We must use and for properly query and get correct result
358
                $relationMethod = "whereRaw";
359
            }
360
361
            if ($relationType instanceof MorphToMany) {
362
                $query->{$relationMethod}($builder, [$relationType->getMorphClass(), $this->prepareKeyword($keyword)]);
363
            } else {
364
                $query->{$relationMethod}($builder, [$this->prepareKeyword($keyword)]);
365
            }
366
        }
367
    }
368
369
    /**
370
     * Compile query builder where clause depending on configurations.
371
     *
372
     * @param mixed $query
373
     * @param string $column
374
     * @param string $keyword
375
     * @param string $relation
376
     */
377
    protected function compileQuerySearch($query, $column, $keyword, $relation = 'or')
378
    {
379
        $column = $this->addTablePrefix($query, $column);
380
        $column = $this->castColumn($column);
381
        $sql    = $column . ' LIKE ?';
382
383
        if ($this->isCaseInsensitive()) {
384
            $sql = 'LOWER(' . $column . ') LIKE ?';
385
        }
386
387
        $query->{$relation . 'WhereRaw'}($sql, [$this->prepareKeyword($keyword)]);
388
    }
389
390
    /**
391
     * Patch for fix about ambiguous field.
392
     * Ambiguous field error will appear when query use join table and search with keyword.
393
     *
394
     * @param mixed $query
395
     * @param string $column
396
     * @return string
397
     */
398
    protected function addTablePrefix($query, $column)
399
    {
400
        // Check if field does not have a table prefix
401
        if (strpos($column, '.') === false) {
402
            // Alternative method to check instanceof \Illuminate\Database\Eloquent\Builder
403
            if (method_exists($query, 'getQuery')) {
404
                $q = $query->getQuery();
405
            } else {
406
                $q = $query;
407
            }
408
409
            if (! $q->from instanceof Expression) {
410
                // Get table from query and add it.
411
                $column = $q->from . '.' . $column;
412
            }
413
        }
414
415
        return $this->wrap($column);
416
    }
417
418
    /**
419
     * Wrap a column and cast in pgsql.
420
     *
421
     * @param  string $column
422
     * @return string
423
     */
424
    protected function castColumn($column)
425
    {
426
        if ($this->database === 'pgsql') {
427
            $column = 'CAST(' . $column . ' as TEXT)';
428
        } elseif ($this->database === 'firebird') {
429
            $column = 'CAST(' . $column . ' as VARCHAR(255))';
430
        }
431
432
        return $column;
433
    }
434
435
    /**
436
     * Prepare search keyword based on configurations.
437
     *
438
     * @param string $keyword
439
     * @return string
440
     */
441
    protected function prepareKeyword($keyword)
442
    {
443
        if ($this->isCaseInsensitive()) {
444
            $keyword = Str::lower($keyword);
445
        }
446
447
        if ($this->isWildcard()) {
448
            $keyword = $this->wildcardLikeString($keyword);
449
        }
450
451
        if ($this->isSmartSearch()) {
452
            $keyword = "%$keyword%";
453
        }
454
455
        return $keyword;
456
    }
457
458
    /**
459
     * Perform column search.
460
     *
461
     * @return void
462
     */
463
    public function columnSearch()
464
    {
465
        $columns = (array) $this->request->input('columns');
466
467
        foreach ($columns as $index => $column) {
468
            if (! $this->request->isColumnSearchable($index)) {
469
                continue;
470
            }
471
472
            $column = $this->getColumnName($index);
473
474
            if (isset($this->columnDef['filter'][$column])) {
475
                $columnDef = $this->columnDef['filter'][$column];
476
                // get a raw keyword (without wildcards)
477
                $keyword = $this->getSearchKeyword($index, true);
478
                $builder = $this->getQueryBuilder();
479
480 View Code Duplication
                if ($columnDef['method'] instanceof Closure) {
481
                    $whereQuery = $builder->newQuery();
482
                    call_user_func_array($columnDef['method'], [$whereQuery, $keyword]);
483
                    $builder->addNestedWhereQuery($whereQuery);
484
                } else {
485
                    $this->compileColumnQuery(
486
                        $builder,
487
                        $columnDef['method'],
488
                        $columnDef['parameters'],
489
                        $column,
490
                        $keyword
491
                    );
492
                }
493 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...
494
                if (count(explode('.', $column)) > 1) {
495
                    $eagerLoads     = $this->getEagerLoads();
496
                    $parts          = explode('.', $column);
497
                    $relationColumn = array_pop($parts);
498
                    $relation       = implode('.', $parts);
499
                    if (in_array($relation, $eagerLoads)) {
500
                        $column = $this->joinEagerLoadedColumn($relation, $relationColumn);
501
                    }
502
                }
503
504
                $keyword = $this->getSearchKeyword($index);
505
                $this->compileColumnSearch($index, $column, $keyword);
506
            }
507
508
            $this->isFilterApplied = true;
509
        }
510
    }
511
512
    /**
513
     * Get proper keyword to use for search.
514
     *
515
     * @param int $i
516
     * @param bool $raw
517
     * @return string
518
     */
519
    protected function getSearchKeyword($i, $raw = false)
520
    {
521
        $keyword = $this->request->columnKeyword($i);
522
        if ($raw || $this->request->isRegex($i)) {
523
            return $keyword;
524
        }
525
526
        return $this->setupKeyword($keyword);
527
    }
528
529
    /**
530
     * Join eager loaded relation and get the related column name.
531
     *
532
     * @param string $relation
533
     * @param string $relationColumn
534
     * @return string
535
     */
536
    protected function joinEagerLoadedColumn($relation, $relationColumn)
537
    {
538
        $model = $this->query->getRelation($relation);
539
        switch (true) {
540
            case $model instanceof BelongsToMany:
541
                $pivot   = $model->getTable();
542
                $pivotPK = $model->getExistenceCompareKey();
543
                $pivotFK = $model->getQualifiedParentKeyName();
544
                $this->performJoin($pivot, $pivotPK, $pivotFK);
545
546
                $related = $model->getRelated();
547
                $table   = $related->getTable();
548
                $tablePK = $related->getForeignKey();
549
                $foreign = $pivot . '.' . $tablePK;
550
                $other   = $related->getQualifiedKeyName();
551
                break;
552
553
            case $model instanceof HasOneOrMany:
554
                $table   = $model->getRelated()->getTable();
555
                $foreign = $model->getQualifiedForeignKeyName();
556
                $other   = $model->getQualifiedParentKeyName();
557
                break;
558
559
            default:
560
                $table   = $model->getRelated()->getTable();
561
                $foreign = $model->getQualifiedForeignKey();
562
                $other   = $model->getQualifiedOtherKeyName();
563
        }
564
565
        $this->performJoin($table, $foreign, $other);
566
567
        return $table . '.' . $relationColumn;
568
    }
569
570
    /**
571
     * Perform join query.
572
     *
573
     * @param string $table
574
     * @param string $foreign
575
     * @param string $other
576
     */
577
    protected function performJoin($table, $foreign, $other)
578
    {
579
        $joins = [];
580
        foreach ((array) $this->getQueryBuilder()->joins as $key => $join) {
581
            $joins[] = $join->table;
582
        }
583
584
        if (! in_array($table, $joins)) {
585
            $this->getQueryBuilder()->join($table, $foreign, '=', $other);
586
        }
587
    }
588
589
    /**
590
     * Compile queries for column search.
591
     *
592
     * @param int $i
593
     * @param mixed $column
594
     * @param string $keyword
595
     */
596
    protected function compileColumnSearch($i, $column, $keyword)
597
    {
598
        if ($this->request->isRegex($i)) {
599
            $column = strstr($column, '(') ? $this->connection->raw($column) : $column;
600
            $this->regexColumnSearch($column, $keyword);
601
        } else {
602
            $this->compileQuerySearch($this->query, $column, $keyword, '');
603
        }
604
    }
605
606
    /**
607
     * Compile regex query column search.
608
     *
609
     * @param mixed $column
610
     * @param string $keyword
611
     */
612
    protected function regexColumnSearch($column, $keyword)
613
    {
614
        if ($this->isOracleSql()) {
615
            $sql = ! $this->isCaseInsensitive() ? 'REGEXP_LIKE( ' . $column . ' , ? )' : 'REGEXP_LIKE( LOWER(' . $column . ') , ?, \'i\' )';
616
            $this->query->whereRaw($sql, [$keyword]);
617
        } else {
618
            $sql = ! $this->isCaseInsensitive() ? $column . ' REGEXP ?' : 'LOWER(' . $column . ') REGEXP ?';
619
            $this->query->whereRaw($sql, [Str::lower($keyword)]);
620
        }
621
    }
622
623
    /**
624
     * Perform sorting of columns.
625
     *
626
     * @return void
627
     */
628
    public function ordering()
629
    {
630
        if ($this->orderCallback) {
631
            call_user_func($this->orderCallback, $this->getQueryBuilder());
632
633
            return;
634
        }
635
636
        foreach ($this->request->orderableColumns() as $orderable) {
637
            $column = $this->getColumnName($orderable['column'], true);
638
639
            if ($this->isBlacklisted($column)) {
640
                continue;
641
            }
642
643
            if (isset($this->columnDef['order'][$column])) {
644
                $method     = $this->columnDef['order'][$column]['method'];
645
                $parameters = $this->columnDef['order'][$column]['parameters'];
646
                $this->compileColumnQuery(
647
                    $this->getQueryBuilder(),
648
                    $method,
649
                    $parameters,
650
                    $column,
651
                    $orderable['direction']
652
                );
653
            } else {
654
                $valid = 1;
655
                if (count(explode('.', $column)) > 1) {
656
                    $eagerLoads     = $this->getEagerLoads();
657
                    $parts          = explode('.', $column);
658
                    $relationColumn = array_pop($parts);
659
                    $relation       = implode('.', $parts);
660
661
                    if (in_array($relation, $eagerLoads)) {
662
                        $relationship = $this->query->getRelation($relation);
663
                        if (! ($relationship instanceof MorphToMany)) {
664
                            $column = $this->joinEagerLoadedColumn($relation, $relationColumn);
665
                        } else {
666
                            $valid = 0;
667
                        }
668
                    }
669
                }
670
671
                if ($valid == 1) {
672
                    if ($this->nullsLast) {
673
                        $this->getQueryBuilder()->orderByRaw($this->getNullsLastSql($column, $orderable['direction']));
674
                    } else {
675
                        $this->getQueryBuilder()->orderBy($column, $orderable['direction']);
676
                    }
677
                }
678
            }
679
        }
680
    }
681
682
    /**
683
     * Get NULLS LAST SQL.
684
     *
685
     * @param  string $column
686
     * @param  string $direction
687
     * @return string
688
     */
689
    protected function getNullsLastSql($column, $direction)
690
    {
691
        $sql = Config::get('datatables.nulls_last_sql', '%s %s NULLS LAST');
692
693
        return sprintf($sql, $column, $direction);
694
    }
695
696
    /**
697
     * Perform pagination
698
     *
699
     * @return void
700
     */
701
    public function paging()
702
    {
703
        $this->query->skip($this->request->input('start'))
704
                    ->take((int) $this->request->input('length') > 0 ? $this->request->input('length') : 10);
705
    }
706
707
    /**
708
     * Get results
709
     *
710
     * @return array|static[]
711
     */
712
    public function results()
713
    {
714
        return $this->query->get();
715
    }
716
}
717