Completed
Push — master ( e96c6d...2336af )
by Arjay
01:52
created

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