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