Completed
Push — master ( 95fc21...45b368 )
by Arjay
12:29 queued 21s
created

QueryDataTable::attachAppends()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 1
dl 0
loc 13
rs 9.4285
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
     * Can the DataTable engine be created with these parameters.
50
     *
51
     * @param mixed $source
52
     * @return bool
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 int
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
            $builder->setBindings([], 'select');
152
        }
153
154
        return $builder;
155
    }
156
157
    /**
158
     * Check if builder query uses complex sql.
159
     *
160
     * @param \Illuminate\Database\Query\Builder $builder
161
     * @return bool
162
     */
163
    protected function isComplexQuery($builder)
164
    {
165
        return Str::contains(Str::lower($builder->toSql()), ['union', 'having', 'distinct', 'order by', 'group by']);
166
    }
167
168
    /**
169
     * Wrap column with DB grammar.
170
     *
171
     * @param string $column
172
     * @return string
173
     */
174
    protected function wrap($column)
175
    {
176
        return $this->connection->getQueryGrammar()->wrap($column);
177
    }
178
179
    /**
180
     * Get paginated results.
181
     *
182
     * @return \Illuminate\Support\Collection
183
     */
184
    public function results()
185
    {
186
        return $this->query->get();
187
    }
188
189
    /**
190
     * Get filtered, ordered and paginated query.
191
     *
192
     * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder
193
     */
194
    public function getFilteredQuery()
195
    {
196
        $this->prepareQuery();
197
198
        return $this->getQuery();
199
    }
200
201
    /**
202
     * Get query builder instance.
203
     *
204
     * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder
205
     */
206
    public function getQuery()
207
    {
208
        return $this->query;
209
    }
210
211
    /**
212
     * Perform column search.
213
     *
214
     * @return void
215
     */
216
    public function columnSearch()
217
    {
218
        $columns = $this->request->columns();
219
220
        foreach ($columns as $index => $column) {
221
            if (! $this->request->isColumnSearchable($index)) {
222
                continue;
223
            }
224
225
            $column = $this->getColumnName($index);
226
227
            if ($this->hasFilterColumn($column)) {
228
                $keyword = $this->getColumnSearchKeyword($index, $raw = true);
229
                $this->applyFilterColumn($this->getBaseQueryBuilder(), $column, $keyword);
230
            } else {
231
                $column  = $this->resolveRelationColumn($column);
232
                $keyword = $this->getColumnSearchKeyword($index);
233
                $this->compileColumnSearch($index, $column, $keyword);
234
            }
235
236
            $this->isFilterApplied = true;
237
        }
238
    }
239
240
    /**
241
     * Check if column has custom filter handler.
242
     *
243
     * @param  string $columnName
244
     * @return bool
245
     */
246
    public function hasFilterColumn($columnName)
247
    {
248
        return isset($this->columnDef['filter'][$columnName]);
249
    }
250
251
    /**
252
     * Get column keyword to use for search.
253
     *
254
     * @param int  $i
255
     * @param bool $raw
256
     * @return string
257
     */
258
    protected function getColumnSearchKeyword($i, $raw = false)
259
    {
260
        $keyword = $this->request->columnKeyword($i);
261
        if ($raw || $this->request->isRegex($i)) {
262
            return $keyword;
263
        }
264
265
        return $this->setupKeyword($keyword);
266
    }
267
268
    /**
269
     * Apply filterColumn api search.
270
     *
271
     * @param mixed  $query
272
     * @param string $columnName
273
     * @param string $keyword
274
     * @param string $boolean
275
     */
276
    protected function applyFilterColumn($query, $columnName, $keyword, $boolean = 'and')
277
    {
278
        $query    = $this->getBaseQueryBuilder($query);
279
        $callback = $this->columnDef['filter'][$columnName]['method'];
280
281
        if ($this->query instanceof EloquentBuilder) {
282
            $builder = $this->query->newModelInstance()->newQuery();
283
        } else {
284
            $builder = $this->query->newQuery();
285
        }
286
287
        $callback($builder, $keyword);
288
289
        $query->addNestedWhereQuery($this->getBaseQueryBuilder($builder), $boolean);
290
    }
291
292
    /**
293
     * Get the base query builder instance.
294
     *
295
     * @param mixed $instance
296
     * @return \Illuminate\Database\Query\Builder
297
     */
298
    protected function getBaseQueryBuilder($instance = null)
299
    {
300
        if (! $instance) {
301
            $instance = $this->query;
302
        }
303
304
        if ($instance instanceof EloquentBuilder) {
305
            return $instance->getQuery();
306
        }
307
308
        return $instance;
309
    }
310
311
    /**
312
     * Resolve the proper column name be used.
313
     *
314
     * @param string $column
315
     * @return string
316
     */
317
    protected function resolveRelationColumn($column)
318
    {
319
        return $column;
320
    }
321
322
    /**
323
     * Compile queries for column search.
324
     *
325
     * @param int    $i
326
     * @param string $column
327
     * @param string $keyword
328
     */
329
    protected function compileColumnSearch($i, $column, $keyword)
330
    {
331
        if ($this->request->isRegex($i)) {
332
            $column = strstr($column, '(') ? $this->connection->raw($column) : $column;
333
            $this->regexColumnSearch($column, $keyword);
334
        } else {
335
            $this->compileQuerySearch($this->query, $column, $keyword, '');
336
        }
337
    }
338
339
    /**
340
     * Compile regex query column search.
341
     *
342
     * @param mixed  $column
343
     * @param string $keyword
344
     */
345
    protected function regexColumnSearch($column, $keyword)
346
    {
347
        switch ($this->connection->getDriverName()) {
348
            case 'oracle':
349
                $sql = ! $this->config->isCaseInsensitive()
350
                    ? 'REGEXP_LIKE( ' . $column . ' , ? )'
351
                    : 'REGEXP_LIKE( LOWER(' . $column . ') , ?, \'i\' )';
352
                break;
353
354
            case 'pgsql':
355
                $column = $this->castColumn($column);
356
                $sql    = ! $this->config->isCaseInsensitive() ? $column . ' ~ ?' : $column . ' ~* ? ';
357
                break;
358
359
            default:
360
                $sql = ! $this->config->isCaseInsensitive()
361
                    ? $column . ' REGEXP ?'
362
                    : 'LOWER(' . $column . ') REGEXP ?';
363
                $keyword = Str::lower($keyword);
364
        }
365
366
        $this->query->whereRaw($sql, [$keyword]);
367
    }
368
369
    /**
370
     * Wrap a column and cast based on database driver.
371
     *
372
     * @param  string $column
373
     * @return string
374
     */
375
    protected function castColumn($column)
376
    {
377
        switch ($this->connection->getDriverName()) {
378
            case 'pgsql':
379
                return 'CAST(' . $column . ' as TEXT)';
380
            case 'firebird':
381
                return 'CAST(' . $column . ' as VARCHAR(255))';
382
            default:
383
                return $column;
384
        }
385
    }
386
387
    /**
388
     * Compile query builder where clause depending on configurations.
389
     *
390
     * @param mixed  $query
391
     * @param string $column
392
     * @param string $keyword
393
     * @param string $boolean
394
     */
395
    protected function compileQuerySearch($query, $column, $keyword, $boolean = 'or')
396
    {
397
        $column = $this->addTablePrefix($query, $column);
398
        $column = $this->castColumn($column);
399
        $sql    = $column . ' LIKE ?';
400
401
        if ($this->config->isCaseInsensitive()) {
402
            $sql = 'LOWER(' . $column . ') LIKE ?';
403
        }
404
405
        $query->{$boolean . 'WhereRaw'}($sql, [$this->prepareKeyword($keyword)]);
406
    }
407
408
    /**
409
     * Patch for fix about ambiguous field.
410
     * Ambiguous field error will appear when query use join table and search with keyword.
411
     *
412
     * @param mixed  $query
413
     * @param string $column
414
     * @return string
415
     */
416
    protected function addTablePrefix($query, $column)
417
    {
418
        if (strpos($column, '.') === false) {
419
            $q = $this->getBaseQueryBuilder($query);
420
            if (! $q->from instanceof Expression) {
421
                $column = $q->from . '.' . $column;
422
            }
423
        }
424
425
        return $this->wrap($column);
426
    }
427
428
    /**
429
     * Prepare search keyword based on configurations.
430
     *
431
     * @param string $keyword
432
     * @return string
433
     */
434
    protected function prepareKeyword($keyword)
435
    {
436
        if ($this->config->isCaseInsensitive()) {
437
            $keyword = Str::lower($keyword);
438
        }
439
440
        if ($this->config->isWildcard()) {
441
            $keyword = Helper::wildcardLikeString($keyword);
442
        }
443
444
        if ($this->config->isSmartSearch()) {
445
            $keyword = "%$keyword%";
446
        }
447
448
        return $keyword;
449
    }
450
451
    /**
452
     * Add custom filter handler for the give column.
453
     *
454
     * @param string   $column
455
     * @param callable $callback
456
     * @return $this
457
     */
458
    public function filterColumn($column, callable $callback)
459
    {
460
        $this->columnDef['filter'][$column] = ['method' => $callback];
461
462
        return $this;
463
    }
464
465
    /**
466
     * Order each given columns versus the given custom sql.
467
     *
468
     * @param array  $columns
469
     * @param string $sql
470
     * @param array  $bindings
471
     * @return $this
472
     */
473
    public function orderColumns(array $columns, $sql, $bindings = [])
474
    {
475
        foreach ($columns as $column) {
476
            $this->orderColumn($column, str_replace(':column', $column, $sql), $bindings);
477
        }
478
479
        return $this;
480
    }
481
482
    /**
483
     * Override default column ordering.
484
     *
485
     * @param string $column
486
     * @param string $sql
487
     * @param array  $bindings
488
     * @return $this
489
     * @internal string $1 Special variable that returns the requested order direction of the column.
490
     */
491
    public function orderColumn($column, $sql, $bindings = [])
492
    {
493
        $this->columnDef['order'][$column] = compact('sql', 'bindings');
494
495
        return $this;
496
    }
497
498
    /**
499
     * Set datatables to do ordering with NULLS LAST option.
500
     *
501
     * @return $this
502
     */
503
    public function orderByNullsLast()
504
    {
505
        $this->nullsLast = true;
506
507
        return $this;
508
    }
509
510
    /**
511
     * Paginate dataTable using limit without offset
512
     * with additional where clause via callback.
513
     *
514
     * @param callable $callback
515
     * @return $this
516
     */
517
    public function limit(callable $callback)
518
    {
519
        $this->limitCallback = $callback;
520
521
        return $this;
522
    }
523
524
    /**
525
     * Perform pagination.
526
     *
527
     * @return void
528
     */
529
    public function paging()
530
    {
531
        $limit = (int) $this->request->input('length') > 0 ? $this->request->input('length') : 10;
532
        if (is_callable($this->limitCallback)) {
533
            $this->query->limit($limit);
534
            call_user_func_array($this->limitCallback, [$this->query]);
535
        } else {
536
            $this->query->skip($this->request->input('start'))->take($limit);
537
        }
538
    }
539
540
    /**
541
     * Add column in collection.
542
     *
543
     * @param string          $name
544
     * @param string|callable $content
545
     * @param bool|int        $order
546
     * @return $this
547
     */
548
    public function addColumn($name, $content, $order = false)
549
    {
550
        $this->pushToBlacklist($name);
551
552
        return parent::addColumn($name, $content, $order);
553
    }
554
555
    /**
556
     * Resolve callback parameter instance.
557
     *
558
     * @return \Illuminate\Database\Query\Builder
559
     */
560
    protected function resolveCallbackParameter()
561
    {
562
        return $this->query;
563
    }
564
565
    /**
566
     * Perform default query orderBy clause.
567
     */
568
    protected function defaultOrdering()
569
    {
570
        collect($this->request->orderableColumns())
571
            ->map(function ($orderable) {
572
                $orderable['name'] = $this->getColumnName($orderable['column'], true);
573
574
                return $orderable;
575
            })
576
            ->reject(function ($orderable) {
577
                return $this->isBlacklisted($orderable['name']) && ! $this->hasOrderColumn($orderable['name']);
578
            })
579
            ->each(function ($orderable) {
580
                $column = $this->resolveRelationColumn($orderable['name']);
581
582
                if ($this->hasOrderColumn($column)) {
583
                    $this->applyOrderColumn($column, $orderable);
584
                } else {
585
                    $nullsLastSql = $this->getNullsLastSql($column, $orderable['direction']);
586
                    $normalSql = $this->wrap($column) . ' ' . $orderable['direction'];
587
                    $sql = $this->nullsLast ? $nullsLastSql : $normalSql;
588
                    $this->query->orderByRaw($sql);
589
                }
590
            });
591
    }
592
593
    /**
594
     * Check if column has custom sort handler.
595
     *
596
     * @param string $column
597
     * @return bool
598
     */
599
    protected function hasOrderColumn($column)
600
    {
601
        return isset($this->columnDef['order'][$column]);
602
    }
603
604
    /**
605
     * Apply orderColumn custom query.
606
     *
607
     * @param string $column
608
     * @param array  $orderable
609
     */
610
    protected function applyOrderColumn($column, $orderable)
611
    {
612
        $sql      = $this->columnDef['order'][$column]['sql'];
613
        $sql      = str_replace('$1', $orderable['direction'], $sql);
614
        $bindings = $this->columnDef['order'][$column]['bindings'];
615
        $this->query->orderByRaw($sql, $bindings);
616
    }
617
618
    /**
619
     * Get NULLS LAST SQL.
620
     *
621
     * @param  string $column
622
     * @param  string $direction
623
     * @return string
624
     */
625
    protected function getNullsLastSql($column, $direction)
626
    {
627
        $sql = $this->config->get('datatables.nulls_last_sql', '%s %s NULLS LAST');
628
629
        return sprintf($sql, $column, $direction);
630
    }
631
632
    /**
633
     * Perform global search for the given keyword.
634
     *
635
     * @param string $keyword
636
     */
637
    protected function globalSearch($keyword)
638
    {
639
        $this->query->where(function ($query) use ($keyword) {
640
            collect($this->request->searchableColumnIndex())
641
                ->map(function ($index) {
642
                    return $this->getColumnName($index);
643
                })
644
                ->reject(function ($column) {
645
                    return $this->isBlacklisted($column) && ! $this->hasFilterColumn($column);
646
                })
647
                ->each(function ($column) use ($keyword, $query) {
648
                    if ($this->hasFilterColumn($column)) {
649
                        $this->applyFilterColumn($query, $column, $keyword, 'or');
650
                    } else {
651
                        $this->compileQuerySearch($query, $column, $keyword);
652
                    }
653
654
                    $this->isFilterApplied = true;
655
                });
656
        });
657
    }
658
659
    /**
660
     * Append debug parameters on output.
661
     *
662
     * @param  array $output
663
     * @return array
664
     */
665
    protected function showDebugger(array $output)
666
    {
667
        $output['queries'] = $this->connection->getQueryLog();
668
        $output['input']   = $this->request->all();
669
670
        return $output;
671
    }
672
673
    /**
674
     * Attach custom with meta on response.
675
     *
676
     * @param array $data
677
     * @return array
678
     */
679
    protected function attachAppends(array $data)
680
    {
681
        $appends = [];
682
        foreach ($this->appends as $key => $value) {
683
            if (is_callable($value)) {
684
                $appends[$key] = value($value($this->getFilteredQuery()));
685
            } else {
686
                $appends[$key] = $value;
687
            }
688
        }
689
690
        return array_merge($data, $appends);
691
    }
692
}
693