Completed
Push — master ( f58995...61d3f4 )
by Arjay
01:46
created

QueryBuilderEngine::getQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Yajra\Datatables\Engines;
4
5
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
6
use Illuminate\Database\Eloquent\Relations\BelongsTo;
7
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
8
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
9
use Illuminate\Database\Eloquent\Relations\MorphToMany;
10
use Illuminate\Database\Query\Builder;
11
use Illuminate\Database\Query\Expression;
12
use Illuminate\Support\Str;
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
     * Builder object.
24
     *
25
     * @var \Illuminate\Database\Query\Builder
26
     */
27
    protected $query;
28
29
    /**
30
     * Database connection used.
31
     *
32
     * @var \Illuminate\Database\Connection
33
     */
34
    protected $connection;
35
36
    /**
37
     * @param \Illuminate\Database\Query\Builder $builder
38
     */
39
    public function __construct(Builder $builder)
40
    {
41
        $this->query      = $builder;
42
        $this->request    = resolve('datatables.request');
43
        $this->config     = resolve('datatables.config');
44
        $this->columns    = $builder->columns;
45
        $this->connection = $builder->getConnection();
46
        if ($this->config->isDebugging()) {
47
            $this->connection->enableQueryLog();
48
        }
49
    }
50
51
    /**
52
     * Set auto filter off and run your own filter.
53
     * Overrides global search.
54
     *
55
     * @param callable $callback
56
     * @param bool     $globalSearch
57
     * @return $this
58
     */
59
    public function filter(callable $callback, $globalSearch = false)
60
    {
61
        $this->overrideGlobalSearch($callback, $this->query, $globalSearch);
62
63
        return $this;
64
    }
65
66
    /**
67
     * Perform global search for the given keyword.
68
     *
69
     * @param string $keyword
70
     */
71
    protected function globalSearch($keyword)
72
    {
73
        $this->query->where(function ($query) use ($keyword) {
74
            $query = $this->getBaseQueryBuilder($query);
75
76
            foreach ($this->request->searchableColumnIndex() as $index) {
77
                $columnName = $this->getColumnName($index);
78
                if ($this->isBlacklisted($columnName) && !$this->hasCustomFilter($columnName)) {
79
                    continue;
80
                }
81
82
                if ($this->hasCustomFilter($columnName)) {
83
                    $this->applyFilterColumn($query, $columnName, $keyword, 'or');
84
                } else {
85
                    $this->compileQuerySearch($query, $columnName, $keyword);
86
                }
87
88
                $this->isFilterApplied = true;
89
            }
90
        });
91
    }
92
93
    /**
94
     * Get the base query builder instance.
95
     *
96
     * @param mixed $instance
97
     * @return \Illuminate\Database\Query\Builder
98
     */
99
    protected function getBaseQueryBuilder($instance = null)
100
    {
101
        if (!$instance) {
102
            $instance = $this->query;
103
        }
104
105
        if ($instance instanceof EloquentBuilder) {
106
            return $instance->getQuery();
107
        }
108
109
        return $instance;
110
    }
111
112
    /**
113
     * Check if column has custom filter handler.
114
     *
115
     * @param  string $columnName
116
     * @return bool
117
     */
118
    public function hasCustomFilter($columnName)
119
    {
120
        return isset($this->columnDef['filter'][$columnName]);
121
    }
122
123
    /**
124
     * Apply filterColumn api search.
125
     *
126
     * @param mixed  $query
127
     * @param string $columnName
128
     * @param string $keyword
129
     * @param string $boolean
130
     */
131
    protected function applyFilterColumn($query, $columnName, $keyword, $boolean = 'and')
132
    {
133
        $callback = $this->columnDef['filter'][$columnName]['method'];
134
        $builder  = $query->newQuery();
135
        $callback($builder, $keyword);
136
        $query->addNestedWhereQuery($builder, $boolean);
137
    }
138
139
    /**
140
     * Compile query builder where clause depending on configurations.
141
     *
142
     * @param mixed  $query
143
     * @param string $column
144
     * @param string $keyword
145
     * @param string $relation
146
     */
147
    protected function compileQuerySearch($query, $column, $keyword, $relation = 'or')
148
    {
149
        $column = $this->addTablePrefix($query, $column);
150
        $column = $this->castColumn($column);
151
        $sql    = $column . ' LIKE ?';
152
153
        if ($this->config->isCaseInsensitive()) {
154
            $sql = 'LOWER(' . $column . ') LIKE ?';
155
        }
156
157
        $query->{$relation . 'WhereRaw'}($sql, [$this->prepareKeyword($keyword)]);
158
    }
159
160
    /**
161
     * Patch for fix about ambiguous field.
162
     * Ambiguous field error will appear when query use join table and search with keyword.
163
     *
164
     * @param mixed  $query
165
     * @param string $column
166
     * @return string
167
     */
168
    protected function addTablePrefix($query, $column)
169
    {
170
        if (strpos($column, '.') === false) {
171
            $q = $this->getBaseQueryBuilder($query);
172
            if (!$q->from instanceof Expression) {
173
                $column = $q->from . '.' . $column;
174
            }
175
        }
176
177
        return $this->wrap($column);
178
    }
179
180
    /**
181
     * Wrap column with DB grammar.
182
     *
183
     * @param string $column
184
     * @return string
185
     */
186
    protected function wrap($column)
187
    {
188
        return $this->connection->getQueryGrammar()->wrap($column);
189
    }
190
191
    /**
192
     * Wrap a column and cast based on database driver.
193
     *
194
     * @param  string $column
195
     * @return string
196
     */
197
    protected function castColumn($column)
198
    {
199
        switch ($this->connection->getDriverName()) {
200
            case 'pgsql':
201
                return 'CAST(' . $column . ' as TEXT)';
202
            case 'firebird':
203
                return 'CAST(' . $column . ' as VARCHAR(255))';
204
            default:
205
                return $column;
206
        }
207
    }
208
209
    /**
210
     * Prepare search keyword based on configurations.
211
     *
212
     * @param string $keyword
213
     * @return string
214
     */
215
    protected function prepareKeyword($keyword)
216
    {
217
        if ($this->config->isCaseInsensitive()) {
218
            $keyword = Str::lower($keyword);
219
        }
220
221
        if ($this->config->isWildcard()) {
222
            $keyword = $this->wildcardLikeString($keyword);
223
        }
224
225
        if ($this->config->isSmartSearch()) {
226
            $keyword = "%$keyword%";
227
        }
228
229
        return $keyword;
230
    }
231
232
    /**
233
     * Organizes works.
234
     *
235
     * @param bool $mDataSupport
236
     * @return \Illuminate\Http\JsonResponse
237
     * @throws \Exception
238
     */
239
    public function make($mDataSupport = false)
240
    {
241
        try {
242
            $this->totalRecords = $this->totalCount();
243
244
            if ($this->totalRecords) {
245
                $this->filterRecords();
246
                $this->ordering();
247
                $this->paginate();
248
            }
249
250
            $data = $this->transform($this->getProcessedData($mDataSupport));
251
252
            return $this->render($data);
253
        } catch (\Exception $exception) {
254
            return $this->errorResponse($exception);
255
        }
256
    }
257
258
    /**
259
     * Count total items.
260
     *
261
     * @return integer
262
     */
263
    public function totalCount()
264
    {
265
        return $this->totalRecords ? $this->totalRecords : $this->count();
266
    }
267
268
    /**
269
     * Counts current query.
270
     *
271
     * @return int
272
     */
273
    public function count()
274
    {
275
        $builder = $this->prepareCountQuery();
276
        $table   = $this->connection->raw('(' . $builder->toSql() . ') count_row_table');
277
278
        return $this->connection->table($table)
279
                                ->setBindings($builder->getBindings())
280
                                ->count();
281
    }
282
283
    /**
284
     * Prepare count query builder.
285
     *
286
     * @return \Illuminate\Database\Query\Builder
287
     */
288
    protected function prepareCountQuery()
289
    {
290
        $builder = clone $this->query;
291
292
        if ($this->isComplexQuery($builder)) {
293
            $row_count = $this->wrap('row_count');
294
            $builder->select($this->connection->raw("'1' as {$row_count}"));
295
        }
296
297
        return $builder;
298
    }
299
300
    /**
301
     * Check if builder query uses complex sql.
302
     *
303
     * @param \Illuminate\Database\Query\Builder $builder
304
     * @return bool
305
     */
306
    protected function isComplexQuery($builder)
307
    {
308
        return !Str::contains(Str::lower($builder->toSql()), ['union', 'having', 'distinct', 'order by', 'group by']);
309
    }
310
311
    /**
312
     * Perform sorting of columns.
313
     *
314
     * @return void
315
     */
316
    public function ordering()
317
    {
318
        if ($this->orderCallback) {
319
            call_user_func($this->orderCallback, $this->getBaseQueryBuilder());
320
321
            return;
322
        }
323
324
        foreach ($this->request->orderableColumns() as $orderable) {
325
            $column = $this->getColumnName($orderable['column'], true);
326
327
            if ($this->isBlacklisted($column) && !$this->hasCustomOrder($column)) {
328
                continue;
329
            }
330
331
            if ($this->hasCustomOrder($column)) {
332
                $sql      = $this->columnDef['order'][$column]['sql'];
333
                $sql      = str_replace('$1', $orderable['direction'], $sql);
334
                $bindings = $this->columnDef['order'][$column]['bindings'];
335
                $this->query->orderByRaw($sql, $bindings);
336
            } else {
337
                $valid = 1;
338
                if (count(explode('.', $column)) > 1) {
339
                    $eagerLoads     = $this->getEagerLoads();
340
                    $parts          = explode('.', $column);
341
                    $relationColumn = array_pop($parts);
342
                    $relation       = implode('.', $parts);
343
344
                    if (in_array($relation, $eagerLoads)) {
345
                        // Loop for nested relations
346
                        // This code is check morph many or not.
347
                        // If one of nested relation is MorphToMany
348
                        // we will call joinEagerLoadedColumn.
349
                        $lastQuery     = $this->query;
350
                        $isMorphToMany = false;
351
                        foreach (explode('.', $relation) as $eachRelation) {
352
                            $relationship = $lastQuery->getRelation($eachRelation);
353
                            if (!($relationship instanceof MorphToMany)) {
354
                                $isMorphToMany = true;
355
                            }
356
                            $lastQuery = $relationship;
357
                        }
358
                        if ($isMorphToMany) {
359
                            $column = $this->joinEagerLoadedColumn($relation, $relationColumn);
360
                        } else {
361
                            $valid = 0;
362
                        }
363
                    }
364
                }
365
366
                if ($valid == 1) {
367
                    if ($this->nullsLast) {
368
                        $this->getBaseQueryBuilder()->orderByRaw($this->getNullsLastSql($column,
369
                            $orderable['direction']));
370
                    } else {
371
                        $this->getBaseQueryBuilder()->orderBy($column, $orderable['direction']);
372
                    }
373
                }
374
            }
375
        }
376
    }
377
378
    /**
379
     * Check if column has custom sort handler.
380
     *
381
     * @param string $column
382
     * @return bool
383
     */
384
    protected function hasCustomOrder($column)
385
    {
386
        return isset($this->columnDef['order'][$column]);
387
    }
388
389
    /**
390
     * Get eager loads keys if eloquent.
391
     *
392
     * @return array
393
     */
394
    protected function getEagerLoads()
395
    {
396
        if ($this->query instanceof EloquentBuilder) {
397
            return array_keys($this->query->getEagerLoads());
398
        }
399
400
        return [];
401
    }
402
403
    /**
404
     * Join eager loaded relation and get the related column name.
405
     *
406
     * @param string $relation
407
     * @param string $relationColumn
408
     * @return string
409
     */
410
    protected function joinEagerLoadedColumn($relation, $relationColumn)
411
    {
412
        $table     = '';
413
        $lastQuery = $this->query;
414
        foreach (explode('.', $relation) as $eachRelation) {
415
            $model = $lastQuery->getRelation($eachRelation);
416
            switch (true) {
417
                case $model instanceof BelongsToMany:
418
                    $pivot   = $model->getTable();
419
                    $pivotPK = $model->getExistenceCompareKey();
420
                    $pivotFK = $model->getQualifiedParentKeyName();
421
                    $this->performJoin($pivot, $pivotPK, $pivotFK);
422
423
                    $related = $model->getRelated();
424
                    $table   = $related->getTable();
425
                    $tablePK = $related->getForeignKey();
426
                    $foreign = $pivot . '.' . $tablePK;
427
                    $other   = $related->getQualifiedKeyName();
428
429
                    $lastQuery->addSelect($table . '.' . $relationColumn);
430
                    $this->performJoin($table, $foreign, $other);
431
432
                    break;
433
434
                case $model instanceof HasOneOrMany:
435
                    $table   = $model->getRelated()->getTable();
436
                    $foreign = $model->getQualifiedForeignKeyName();
437
                    $other   = $model->getQualifiedParentKeyName();
438
                    break;
439
440
                case $model instanceof BelongsTo:
441
                    $table   = $model->getRelated()->getTable();
442
                    $foreign = $model->getQualifiedForeignKey();
443
                    $other   = $model->getQualifiedOwnerKeyName();
444
                    break;
445
446
                default:
447
                    $table   = $model->getRelated()->getTable();
448
                    $foreign = $model->getQualifiedForeignKey();
449
                    $other   = $model->getQualifiedOtherKeyName();
450
            }
451
            $this->performJoin($table, $foreign, $other);
452
            $lastQuery = $model->getQuery();
453
        }
454
455
        return $table . '.' . $relationColumn;
456
    }
457
458
    /**
459
     * Perform join query.
460
     *
461
     * @param string $table
462
     * @param string $foreign
463
     * @param string $other
464
     */
465
    protected function performJoin($table, $foreign, $other)
466
    {
467
        $joins = [];
468
        foreach ((array) $this->getBaseQueryBuilder()->joins as $key => $join) {
469
            $joins[] = $join->table;
470
        }
471
472
        if (!in_array($table, $joins)) {
473
            $this->getBaseQueryBuilder()->leftJoin($table, $foreign, '=', $other);
474
        }
475
    }
476
477
    /**
478
     * Get NULLS LAST SQL.
479
     *
480
     * @param  string $column
481
     * @param  string $direction
482
     * @return string
483
     */
484
    protected function getNullsLastSql($column, $direction)
485
    {
486
        $sql = $this->config->get('datatables.nulls_last_sql', '%s %s NULLS LAST');
487
488
        return sprintf($sql, $column, $direction);
489
    }
490
491
    /**
492
     * Perform column search.
493
     *
494
     * @return void
495
     */
496
    public function columnSearch()
497
    {
498
        $columns = $this->request->columns();
499
500
        foreach ($columns as $index => $column) {
501
            if (!$this->request->isColumnSearchable($index)) {
502
                continue;
503
            }
504
505
            $column = $this->getColumnName($index);
506
507
            if ($this->hasCustomFilter($column)) {
508
                $keyword = $this->getColumnSearchKeyword($index, $raw = true);
509
                $this->applyFilterColumn($this->query, $column, $keyword);
510
            } else {
511
                if (count(explode('.', $column)) > 1) {
512
                    $eagerLoads     = $this->getEagerLoads();
513
                    $parts          = explode('.', $column);
514
                    $relationColumn = array_pop($parts);
515
                    $relation       = implode('.', $parts);
516
                    if (in_array($relation, $eagerLoads)) {
517
                        $column = $this->joinEagerLoadedColumn($relation, $relationColumn);
518
                    }
519
                }
520
521
                $keyword = $this->getColumnSearchKeyword($index);
522
                $this->compileColumnSearch($index, $column, $keyword);
523
            }
524
525
            $this->isFilterApplied = true;
526
        }
527
    }
528
529
    /**
530
     * Get column keyword to use for search.
531
     *
532
     * @param int  $i
533
     * @param bool $raw
534
     * @return string
535
     */
536
    protected function getColumnSearchKeyword($i, $raw = false)
537
    {
538
        $keyword = $this->request->columnKeyword($i);
539
        if ($raw || $this->request->isRegex($i)) {
540
            return $keyword;
541
        }
542
543
        return $this->setupKeyword($keyword);
544
    }
545
546
    /**
547
     * Compile queries for column search.
548
     *
549
     * @param int    $i
550
     * @param string $column
551
     * @param string $keyword
552
     */
553
    protected function compileColumnSearch($i, $column, $keyword)
554
    {
555
        if ($this->request->isRegex($i)) {
556
            $column = strstr($column, '(') ? $this->connection->raw($column) : $column;
557
            $this->regexColumnSearch($column, $keyword);
558
        } else {
559
            $this->compileQuerySearch($this->query, $column, $keyword, '');
560
        }
561
    }
562
563
    /**
564
     * Compile regex query column search.
565
     *
566
     * @param mixed  $column
567
     * @param string $keyword
568
     */
569
    protected function regexColumnSearch($column, $keyword)
570
    {
571
        switch ($this->connection->getDriverName()) {
572
            case 'oracle':
573
                $sql = !$this->config
574
                             ->isCaseInsensitive() ? 'REGEXP_LIKE( ' . $column . ' , ? )' : 'REGEXP_LIKE( LOWER(' . $column . ') , ?, \'i\' )';
575
                break;
576
577
            case 'pgsql':
578
                $sql = !$this->config->isCaseInsensitive() ? $column . ' ~ ?' : $column . ' ~* ? ';
579
                break;
580
581
            default:
582
                $sql     = !$this->config
583
                                 ->isCaseInsensitive() ? $column . ' REGEXP ?' : 'LOWER(' . $column . ') REGEXP ?';
584
                $keyword = Str::lower($keyword);
585
        }
586
587
        $this->query->whereRaw($sql, [$keyword]);
588
    }
589
590
    /**
591
     * Perform pagination.
592
     *
593
     * @return void
594
     */
595
    public function paging()
596
    {
597
        $this->query->skip($this->request->input('start'))
598
                    ->take((int) $this->request->input('length') > 0 ? $this->request->input('length') : 10);
599
    }
600
601
    /**
602
     * Get paginated results.
603
     *
604
     * @return \Illuminate\Support\Collection
605
     */
606
    public function results()
607
    {
608
        return $this->query->get();
609
    }
610
611
    /**
612
     * Add column in collection.
613
     *
614
     * @param string          $name
615
     * @param string|callable $content
616
     * @param bool|int        $order
617
     * @return \Yajra\Datatables\Engines\BaseEngine|\Yajra\Datatables\Engines\QueryBuilderEngine
618
     */
619
    public function addColumn($name, $content, $order = false)
620
    {
621
        $this->pushToBlacklist($name);
622
623
        return parent::addColumn($name, $content, $order);
624
    }
625
626
    /**
627
     * Get query builder instance.
628
     *
629
     * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder
630
     */
631
    public function getQuery()
632
    {
633
        return $this->query;
634
    }
635
636
    /**
637
     * Append debug parameters on output.
638
     *
639
     * @param  array $output
640
     * @return array
641
     */
642
    protected function showDebugger(array $output)
643
    {
644
        $output['queries'] = $this->connection->getQueryLog();
645
        $output['input']   = $this->request->all();
646
647
        return $output;
648
    }
649
}
650