Completed
Push — master ( 928d8a...2f7023 )
by Arjay
07:15
created

QueryDataTable   C

Complexity

Total Complexity 72

Size/Duplication

Total Lines 623
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
dl 0
loc 623
rs 5.448
c 0
b 0
f 0
wmc 72
lcom 1
cbo 10

36 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 2
A make() 0 14 2
A prepareQuery() 0 14 3
A totalCount() 0 4 2
A count() 0 9 1
A prepareCountQuery() 0 11 2
A isComplexQuery() 0 4 1
A wrap() 0 4 1
A results() 0 4 1
A getFilteredQuery() 0 6 1
A getQuery() 0 4 1
B columnSearch() 0 24 4
A hasFilterColumn() 0 4 1
A getColumnSearchKeyword() 0 9 3
A applyFilterColumn() 0 14 2
A getBaseQueryBuilder() 0 12 3
A resolveRelationColumn() 0 4 1
A compileColumnSearch() 0 9 3
B regexColumnSearch() 0 20 6
A compileQuerySearch() 0 12 2
A addTablePrefix() 0 11 3
A castColumn() 0 11 3
A prepareKeyword() 0 16 4
A filterColumn() 0 6 1
A orderColumns() 0 8 2
A orderColumn() 0 6 1
A orderByNullsLast() 0 6 1
A paging() 0 5 2
A addColumn() 0 6 1
A resolveCallbackParameter() 0 4 1
B defaultOrdering() 0 24 4
A hasOrderColumn() 0 4 1
A applyOrderColumn() 0 7 1
A getNullsLastSql() 0 6 1
A globalSearch() 0 23 3
A showDebugger() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like QueryDataTable often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QueryDataTable, and based on these observations, apply Extract Interface, too.

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
     * @param \Illuminate\Database\Query\Builder $builder
43
     */
44
    public function __construct(Builder $builder)
45
    {
46
        $this->query      = $builder;
47
        $this->request    = resolve('datatables.request');
48
        $this->config     = resolve('datatables.config');
49
        $this->columns    = $builder->columns;
50
        $this->connection = $builder->getConnection();
51
        if ($this->config->isDebugging()) {
52
            $this->connection->enableQueryLog();
53
        }
54
    }
55
56
    /**
57
     * Organizes works.
58
     *
59
     * @param bool $mDataSupport
60
     * @return \Illuminate\Http\JsonResponse
61
     * @throws \Exception
62
     */
63
    public function make($mDataSupport = true)
64
    {
65
        try {
66
            $this->prepareQuery();
67
68
            $results   = $this->results();
69
            $processed = $this->processResults($results, $mDataSupport);
70
            $data      = $this->transform($results, $processed);
71
72
            return $this->render($data);
73
        } catch (\Exception $exception) {
74
            return $this->errorResponse($exception);
75
        }
76
    }
77
78
    /**
79
     * Prepare query by executing count, filter, order and paginate.
80
     */
81
    protected function prepareQuery()
82
    {
83
        if (!$this->prepared) {
84
            $this->totalRecords = $this->totalCount();
85
86
            if ($this->totalRecords) {
87
                $this->filterRecords();
88
                $this->ordering();
89
                $this->paginate();
90
            }
91
        }
92
93
        $this->prepared = true;
94
    }
95
96
    /**
97
     * Count total items.
98
     *
99
     * @return integer
100
     */
101
    public function totalCount()
102
    {
103
        return $this->totalRecords ? $this->totalRecords : $this->count();
104
    }
105
106
    /**
107
     * Counts current query.
108
     *
109
     * @return int
110
     */
111
    public function count()
112
    {
113
        $builder = $this->prepareCountQuery();
114
        $table   = $this->connection->raw('(' . $builder->toSql() . ') count_row_table');
115
116
        return $this->connection->table($table)
117
                                ->setBindings($builder->getBindings())
118
                                ->count();
119
    }
120
121
    /**
122
     * Prepare count query builder.
123
     *
124
     * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder
125
     */
126
    protected function prepareCountQuery()
127
    {
128
        $builder = clone $this->query;
129
130
        if ($this->isComplexQuery($builder)) {
131
            $row_count = $this->wrap('row_count');
132
            $builder->select($this->connection->raw("'1' as {$row_count}"));
133
        }
134
135
        return $builder;
136
    }
137
138
    /**
139
     * Check if builder query uses complex sql.
140
     *
141
     * @param \Illuminate\Database\Query\Builder $builder
142
     * @return bool
143
     */
144
    protected function isComplexQuery($builder)
145
    {
146
        return !Str::contains(Str::lower($builder->toSql()), ['union', 'having', 'distinct', 'order by', 'group by']);
147
    }
148
149
    /**
150
     * Wrap column with DB grammar.
151
     *
152
     * @param string $column
153
     * @return string
154
     */
155
    protected function wrap($column)
156
    {
157
        return $this->connection->getQueryGrammar()->wrap($column);
158
    }
159
160
    /**
161
     * Get paginated results.
162
     *
163
     * @return \Illuminate\Support\Collection
164
     */
165
    public function results()
166
    {
167
        return $this->query->get();
168
    }
169
170
    /**
171
     * Get filtered, ordered and paginated query.
172
     *
173
     * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder
174
     */
175
    public function getFilteredQuery()
176
    {
177
        $this->prepareQuery();
178
179
        return $this->getQuery();
180
    }
181
182
    /**
183
     * Get query builder instance.
184
     *
185
     * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder
186
     */
187
    public function getQuery()
188
    {
189
        return $this->query;
190
    }
191
192
    /**
193
     * Perform column search.
194
     *
195
     * @return void
196
     */
197
    public function columnSearch()
198
    {
199
        $columns = $this->request->columns();
200
201
        foreach ($columns as $index => $column) {
202
            if (!$this->request->isColumnSearchable($index)) {
203
                continue;
204
            }
205
206
            $column = $this->getColumnName($index);
207
208
            if ($this->hasFilterColumn($column)) {
209
                $keyword = $this->getColumnSearchKeyword($index, $raw = true);
210
                $this->applyFilterColumn($this->getBaseQueryBuilder(), $column, $keyword);
211
                continue;
212
            }
213
214
            $column  = $this->resolveRelationColumn($column);
215
            $keyword = $this->getColumnSearchKeyword($index);
216
            $this->compileColumnSearch($index, $column, $keyword);
217
218
            $this->isFilterApplied = true;
219
        }
220
    }
221
222
    /**
223
     * Check if column has custom filter handler.
224
     *
225
     * @param  string $columnName
226
     * @return bool
227
     */
228
    public function hasFilterColumn($columnName)
229
    {
230
        return isset($this->columnDef['filter'][$columnName]);
231
    }
232
233
    /**
234
     * Get column keyword to use for search.
235
     *
236
     * @param int  $i
237
     * @param bool $raw
238
     * @return string
239
     */
240
    protected function getColumnSearchKeyword($i, $raw = false)
241
    {
242
        $keyword = $this->request->columnKeyword($i);
243
        if ($raw || $this->request->isRegex($i)) {
244
            return $keyword;
245
        }
246
247
        return $this->setupKeyword($keyword);
248
    }
249
250
    /**
251
     * Apply filterColumn api search.
252
     *
253
     * @param Builder $query
254
     * @param string  $columnName
255
     * @param string  $keyword
256
     * @param string  $boolean
257
     */
258
    protected function applyFilterColumn(Builder $query, $columnName, $keyword, $boolean = 'and')
259
    {
260
        $callback = $this->columnDef['filter'][$columnName]['method'];
261
262
        if ($this->query instanceof EloquentBuilder) {
263
            $builder = $this->query->newModelInstance()->newQuery();
264
        } else {
265
            $builder = $this->query->newQuery();
266
        }
267
268
        $callback($builder, $keyword);
269
270
        $query->addNestedWhereQuery($this->getBaseQueryBuilder($builder), $boolean);
271
    }
272
273
    /**
274
     * Get the base query builder instance.
275
     *
276
     * @param mixed $instance
277
     * @return \Illuminate\Database\Query\Builder
278
     */
279
    protected function getBaseQueryBuilder($instance = null)
280
    {
281
        if (!$instance) {
282
            $instance = $this->query;
283
        }
284
285
        if ($instance instanceof EloquentBuilder) {
286
            return $instance->getQuery();
287
        }
288
289
        return $instance;
290
    }
291
292
    /**
293
     * Resolve the proper column name be used.
294
     *
295
     * @param string $column
296
     * @return string
297
     */
298
    protected function resolveRelationColumn($column)
299
    {
300
        return $column;
301
    }
302
303
    /**
304
     * Compile queries for column search.
305
     *
306
     * @param int    $i
307
     * @param string $column
308
     * @param string $keyword
309
     */
310
    protected function compileColumnSearch($i, $column, $keyword)
311
    {
312
        if ($this->request->isRegex($i)) {
313
            $column = strstr($column, '(') ? $this->connection->raw($column) : $column;
314
            $this->regexColumnSearch($column, $keyword);
315
        } else {
316
            $this->compileQuerySearch($this->query, $column, $keyword, '');
317
        }
318
    }
319
320
    /**
321
     * Compile regex query column search.
322
     *
323
     * @param mixed  $column
324
     * @param string $keyword
325
     */
326
    protected function regexColumnSearch($column, $keyword)
327
    {
328
        switch ($this->connection->getDriverName()) {
329
            case 'oracle':
330
                $sql = !$this->config
331
                    ->isCaseInsensitive() ? 'REGEXP_LIKE( ' . $column . ' , ? )' : 'REGEXP_LIKE( LOWER(' . $column . ') , ?, \'i\' )';
332
                break;
333
334
            case 'pgsql':
335
                $sql = !$this->config->isCaseInsensitive() ? $column . ' ~ ?' : $column . ' ~* ? ';
336
                break;
337
338
            default:
339
                $sql     = !$this->config
340
                    ->isCaseInsensitive() ? $column . ' REGEXP ?' : 'LOWER(' . $column . ') REGEXP ?';
341
                $keyword = Str::lower($keyword);
342
        }
343
344
        $this->query->whereRaw($sql, [$keyword]);
345
    }
346
347
    /**
348
     * Compile query builder where clause depending on configurations.
349
     *
350
     * @param mixed  $query
351
     * @param string $column
352
     * @param string $keyword
353
     * @param string $relation
354
     */
355
    protected function compileQuerySearch($query, $column, $keyword, $relation = 'or')
356
    {
357
        $column = $this->addTablePrefix($query, $column);
358
        $column = $this->castColumn($column);
359
        $sql    = $column . ' LIKE ?';
360
361
        if ($this->config->isCaseInsensitive()) {
362
            $sql = 'LOWER(' . $column . ') LIKE ?';
363
        }
364
365
        $query->{$relation . 'WhereRaw'}($sql, [$this->prepareKeyword($keyword)]);
366
    }
367
368
    /**
369
     * Patch for fix about ambiguous field.
370
     * Ambiguous field error will appear when query use join table and search with keyword.
371
     *
372
     * @param mixed  $query
373
     * @param string $column
374
     * @return string
375
     */
376
    protected function addTablePrefix($query, $column)
377
    {
378
        if (strpos($column, '.') === false) {
379
            $q = $this->getBaseQueryBuilder($query);
380
            if (!$q->from instanceof Expression) {
381
                $column = $q->from . '.' . $column;
382
            }
383
        }
384
385
        return $this->wrap($column);
386
    }
387
388
    /**
389
     * Wrap a column and cast based on database driver.
390
     *
391
     * @param  string $column
392
     * @return string
393
     */
394
    protected function castColumn($column)
395
    {
396
        switch ($this->connection->getDriverName()) {
397
            case 'pgsql':
398
                return 'CAST(' . $column . ' as TEXT)';
399
            case 'firebird':
400
                return 'CAST(' . $column . ' as VARCHAR(255))';
401
            default:
402
                return $column;
403
        }
404
    }
405
406
    /**
407
     * Prepare search keyword based on configurations.
408
     *
409
     * @param string $keyword
410
     * @return string
411
     */
412
    protected function prepareKeyword($keyword)
413
    {
414
        if ($this->config->isCaseInsensitive()) {
415
            $keyword = Str::lower($keyword);
416
        }
417
418
        if ($this->config->isWildcard()) {
419
            $keyword = Helper::wildcardLikeString($keyword);
420
        }
421
422
        if ($this->config->isSmartSearch()) {
423
            $keyword = "%$keyword%";
424
        }
425
426
        return $keyword;
427
    }
428
429
    /**
430
     * Add custom filter handler for the give column.
431
     *
432
     * @param string   $column
433
     * @param callable $callback
434
     * @return $this
435
     */
436
    public function filterColumn($column, callable $callback)
437
    {
438
        $this->columnDef['filter'][$column] = ['method' => $callback];
439
440
        return $this;
441
    }
442
443
    /**
444
     * Order each given columns versus the given custom sql.
445
     *
446
     * @param array  $columns
447
     * @param string $sql
448
     * @param array  $bindings
449
     * @return $this
450
     */
451
    public function orderColumns(array $columns, $sql, $bindings = [])
452
    {
453
        foreach ($columns as $column) {
454
            $this->orderColumn($column, str_replace(':column', $column, $sql), $bindings);
455
        }
456
457
        return $this;
458
    }
459
460
    /**
461
     * Override default column ordering.
462
     *
463
     * @param string $column
464
     * @param string $sql
465
     * @param array  $bindings
466
     * @return $this
467
     * @internal string $1 Special variable that returns the requested order direction of the column.
468
     */
469
    public function orderColumn($column, $sql, $bindings = [])
470
    {
471
        $this->columnDef['order'][$column] = compact('sql', 'bindings');
472
473
        return $this;
474
    }
475
476
    /**
477
     * Set datatables to do ordering with NULLS LAST option.
478
     *
479
     * @return $this
480
     */
481
    public function orderByNullsLast()
482
    {
483
        $this->nullsLast = true;
484
485
        return $this;
486
    }
487
488
    /**
489
     * Perform pagination.
490
     *
491
     * @return void
492
     */
493
    public function paging()
494
    {
495
        $this->query->skip($this->request->input('start'))
0 ignored issues
show
Documentation Bug introduced by
The method input does not exist on object<Yajra\DataTables\Utilities\Request>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
496
                    ->take((int) $this->request->input('length') > 0 ? $this->request->input('length') : 10);
0 ignored issues
show
Documentation Bug introduced by
The method input does not exist on object<Yajra\DataTables\Utilities\Request>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
497
    }
498
499
    /**
500
     * Add column in collection.
501
     *
502
     * @param string          $name
503
     * @param string|callable $content
504
     * @param bool|int        $order
505
     * @return \Yajra\DataTables\DataTableAbstract|\Yajra\DataTables\Builders\QueryDataTable
506
     */
507
    public function addColumn($name, $content, $order = false)
508
    {
509
        $this->pushToBlacklist($name);
510
511
        return parent::addColumn($name, $content, $order);
512
    }
513
514
    /**
515
     * Resolve callback parameter instance.
516
     *
517
     * @return \Illuminate\Database\Query\Builder
518
     */
519
    protected function resolveCallbackParameter()
520
    {
521
        return $this->query;
522
    }
523
524
    /**
525
     * Perform default query orderBy clause.
526
     */
527
    protected function defaultOrdering()
528
    {
529
        collect($this->request->orderableColumns())
530
            ->map(function ($orderable) {
531
                $orderable['name'] = $this->getColumnName($orderable['column'], true);
532
533
                return $orderable;
534
            })
535
            ->reject(function ($orderable) {
536
                return $this->isBlacklisted($orderable['name']) && !$this->hasOrderColumn($orderable['name']);
537
            })
538
            ->each(function ($orderable) {
539
                $column = $this->resolveRelationColumn($orderable['name']);
540
541
                if ($this->hasOrderColumn($column)) {
542
                    $this->applyOrderColumn($column, $orderable);
543
                } else {
544
                    $nullsLastSql = $this->getNullsLastSql($column, $orderable['direction']);
545
                    $normalSql    = $this->wrap($column) . ' ' . $orderable['direction'];
546
                    $sql          = $this->nullsLast ? $nullsLastSql : $normalSql;
547
                    $this->query->orderByRaw($sql);
548
                }
549
            });
550
    }
551
552
    /**
553
     * Check if column has custom sort handler.
554
     *
555
     * @param string $column
556
     * @return bool
557
     */
558
    protected function hasOrderColumn($column)
559
    {
560
        return isset($this->columnDef['order'][$column]);
561
    }
562
563
    /**
564
     * Apply orderColumn custom query.
565
     *
566
     * @param string $column
567
     * @param array  $orderable
568
     */
569
    protected function applyOrderColumn($column, $orderable): void
570
    {
571
        $sql      = $this->columnDef['order'][$column]['sql'];
572
        $sql      = str_replace('$1', $orderable['direction'], $sql);
573
        $bindings = $this->columnDef['order'][$column]['bindings'];
574
        $this->query->orderByRaw($sql, $bindings);
575
    }
576
577
    /**
578
     * Get NULLS LAST SQL.
579
     *
580
     * @param  string $column
581
     * @param  string $direction
582
     * @return string
583
     */
584
    protected function getNullsLastSql($column, $direction)
585
    {
586
        $sql = $this->config->get('datatables.nulls_last_sql', '%s %s NULLS LAST');
587
588
        return sprintf($sql, $column, $direction);
589
    }
590
591
    /**
592
     * Perform global search for the given keyword.
593
     *
594
     * @param string $keyword
595
     */
596
    protected function globalSearch($keyword)
597
    {
598
        $this->query->where(function ($query) use ($keyword) {
599
            $query = $this->getBaseQueryBuilder($query);
600
601
            collect($this->request->searchableColumnIndex())
602
                ->map(function ($index) {
603
                    return $this->getColumnName($index);
604
                })
605
                ->reject(function ($column) {
606
                    return $this->isBlacklisted($column) && !$this->hasFilterColumn($column);
607
                })
608
                ->each(function ($column) use ($keyword, $query) {
609
                    if ($this->hasFilterColumn($column)) {
610
                        $this->applyFilterColumn($query, $column, $keyword, 'or');
611
                    } else {
612
                        $this->compileQuerySearch($query, $column, $keyword);
613
                    }
614
615
                    $this->isFilterApplied = true;
616
                });
617
        });
618
    }
619
620
    /**
621
     * Append debug parameters on output.
622
     *
623
     * @param  array $output
624
     * @return array
625
     */
626
    protected function showDebugger(array $output)
627
    {
628
        $output['queries'] = $this->connection->getQueryLog();
629
        $output['input']   = $this->request->all();
0 ignored issues
show
Documentation Bug introduced by
The method all does not exist on object<Yajra\DataTables\Utilities\Request>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
630
631
        return $output;
632
    }
633
}
634