Completed
Push — master ( df8c5a...bdecd9 )
by Arjay
9s
created

src/Engines/QueryBuilderEngine.php (1 issue)

Labels
Severity

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

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
562
                $other   = $model->getQualifiedParentKeyName();
563
            } else {
564
                $foreign = $model->getQualifiedForeignKey();
565
                $other   = $model->getQualifiedOtherKeyName();
566
            }
567
568
            if (! in_array($table, $joins)) {
569
                $this->getQueryBuilder()->leftJoin($table, $foreign, '=', $other);
570
            }
571
        }
572
573
        $column = $table . '.' . $relationColumn;
574
575
        return $column;
576
    }
577
578
    /**
579
     * Compile queries for column search.
580
     *
581
     * @param int $i
582
     * @param mixed $column
583
     * @param string $keyword
584
     */
585
    protected function compileColumnSearch($i, $column, $keyword)
586
    {
587
        if ($this->request->isRegex($i)) {
588
            $column = strstr($column, '(') ? $this->connection->raw($column) : $column;
589
            $this->regexColumnSearch($column, $keyword);
590
        } else {
591
            $this->compileQuerySearch($this->query, $column, $keyword, '');
592
        }
593
    }
594
595
    /**
596
     * Compile regex query column search.
597
     *
598
     * @param mixed $column
599
     * @param string $keyword
600
     */
601
    protected function regexColumnSearch($column, $keyword)
602
    {
603
        if ($this->isOracleSql()) {
604
            $sql = ! $this->isCaseInsensitive() ? 'REGEXP_LIKE( ' . $column . ' , ? )' : 'REGEXP_LIKE( LOWER(' . $column . ') , ?, \'i\' )';
605
            $this->query->whereRaw($sql, [$keyword]);
606
        } else {
607
            $sql = ! $this->isCaseInsensitive() ? $column . ' REGEXP ?' : 'LOWER(' . $column . ') REGEXP ?';
608
            $this->query->whereRaw($sql, [Str::lower($keyword)]);
609
        }
610
    }
611
612
    /**
613
     * Perform sorting of columns.
614
     *
615
     * @return void
616
     */
617
    public function ordering()
618
    {
619
        if ($this->orderCallback) {
620
            call_user_func($this->orderCallback, $this->getQueryBuilder());
621
622
            return;
623
        }
624
625
        foreach ($this->request->orderableColumns() as $orderable) {
626
            $column = $this->getColumnName($orderable['column'], true);
627
628
            if ($this->isBlacklisted($column)) {
629
                continue;
630
            }
631
632
            if (isset($this->columnDef['order'][$column])) {
633
                $method     = $this->columnDef['order'][$column]['method'];
634
                $parameters = $this->columnDef['order'][$column]['parameters'];
635
                $this->compileColumnQuery(
636
                    $this->getQueryBuilder(),
637
                    $method,
638
                    $parameters,
639
                    $column,
640
                    $orderable['direction']
641
                );
642
            } else {
643
                $valid = 1;
644
                if (count(explode('.', $column)) > 1) {
645
                    $eagerLoads     = $this->getEagerLoads();
646
                    $parts          = explode('.', $column);
647
                    $relationColumn = array_pop($parts);
648
                    $relation       = implode('.', $parts);
649
650
                    if (in_array($relation, $eagerLoads)) {
651
                        $relationship = $this->query->getRelation($relation);
652
                        if (! ($relationship instanceof MorphToMany)) {
653
                            $column = $this->joinEagerLoadedColumn($relation, $relationColumn);
654
                        } else {
655
                            $valid = 0;
656
                        }
657
                    }
658
                }
659
660
                if ($valid == 1) {
661
                    if ($this->nullsLast) {
662
                        $this->getQueryBuilder()->orderByRaw($this->getNullsLastSql($column, $orderable['direction']));
663
                    } else {
664
                        $this->getQueryBuilder()->orderBy($column, $orderable['direction']);
665
                    }
666
                }
667
            }
668
        }
669
    }
670
671
    /**
672
     * Get NULLS LAST SQL.
673
     *
674
     * @param  string $column
675
     * @param  string $direction
676
     * @return string
677
     */
678
    protected function getNullsLastSql($column, $direction)
679
    {
680
        $sql = Config::get('datatables.nulls_last_sql', '%s %s NULLS LAST');
681
682
        return sprintf($sql, $column, $direction);
683
    }
684
685
    /**
686
     * Perform pagination
687
     *
688
     * @return void
689
     */
690
    public function paging()
691
    {
692
        $this->query->skip($this->request->input('start'))
693
                    ->take((int) $this->request->input('length') > 0 ? $this->request->input('length') : 10);
694
    }
695
696
    /**
697
     * Get results
698
     *
699
     * @return array|static[]
700
     */
701
    public function results()
702
    {
703
        return $this->query->get();
704
    }
705
}
706