Completed
Push — master ( 3b084b...76ea7b )
by Arjay
08:44
created

QueryDataTable::filteredCount()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 0
dl 0
loc 9
rs 9.9666
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
     * Flag to use simple pagination.
50
     *
51
     * @var bool
52
     */
53
    protected $simplePagination = false;
54
55
    /**
56
     * Can the DataTable engine be created with these parameters.
57
     *
58
     * @param mixed $source
59
     * @return bool
60
     */
61
    public static function canCreate($source)
62
    {
63
        return $source instanceof Builder;
64
    }
65
66
    /**
67
     * @param \Illuminate\Database\Query\Builder $builder
68
     */
69
    public function __construct(Builder $builder)
70
    {
71
        $this->query      = $builder;
72
        $this->request    = app('datatables.request');
73
        $this->config     = app('datatables.config');
74
        $this->columns    = $builder->columns;
75
        $this->connection = $builder->getConnection();
76
        if ($this->config->isDebugging()) {
77
            $this->connection->enableQueryLog();
78
        }
79
    }
80
81
    /**
82
     * Organizes works.
83
     *
84
     * @param bool $mDataSupport
85
     * @return \Illuminate\Http\JsonResponse
86
     * @throws \Exception
87
     */
88
    public function make($mDataSupport = true)
89
    {
90
        try {
91
            $this->prepareQuery();
92
93
            $results   = $this->results();
94
            $processed = $this->processResults($results, $mDataSupport);
95
            $data      = $this->transform($results, $processed);
96
97
            return $this->render($data);
98
        } catch (\Exception $exception) {
99
            return $this->errorResponse($exception);
100
        }
101
    }
102
103
    /**
104
     * Prepare query by executing count, filter, order and paginate.
105
     */
106
    protected function prepareQuery()
107
    {
108
        if (! $this->prepared) {
109
            $this->totalRecords = $this->totalCount();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->totalCount() can also be of type boolean. However, the property $totalRecords is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
110
111
            if ($this->totalRecords) {
112
                $this->filterRecords();
113
                $this->ordering();
114
                $this->paginate();
115
            }
116
        }
117
118
        $this->prepared = true;
119
    }
120
121
    /**
122
     * Use simple pagination to set the recordsTotal equals to recordsFiltered.
123
     * This will improve the performance by skipping the count query.
124
     *
125
     * @return $this
126
     */
127
    public function simplePagination()
128
    {
129
        $this->simplePagination = true;
130
131
        return $this;
132
    }
133
134
    /**
135
     * Count total items.
136
     *
137
     * @return int
138
     */
139
    public function totalCount()
140
    {
141
        if ($this->simplePagination) {
142
            return true;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return true; (boolean) is incompatible with the return type declared by the interface Yajra\DataTables\Contracts\DataTable::totalCount of type integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

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