Completed
Push — master ( 7133d1...bb16e7 )
by Arjay
01:47
created

QueryBuilderEngine::compileQuerySearch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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