Completed
Push — master ( e98ec0...b5ecb4 )
by Arjay
07:49
created

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