Completed
Push — master ( 01f863...e04cdd )
by Arjay
11:27
created

QueryBuilderEngine::smartGlobalSearch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 8
rs 9.4285
1
<?php
2
3
namespace Yajra\Datatables\Engines;
4
5
use Closure;
6
use Illuminate\Database\Eloquent\Relations\BelongsTo;
7
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
8
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
9
use Illuminate\Database\Eloquent\Relations\MorphToMany;
10
use Illuminate\Database\Query\Builder;
11
use Illuminate\Database\Query\Expression;
12
use Illuminate\Support\Facades\Config;
13
use Illuminate\Support\Str;
14
use Yajra\Datatables\Helper;
15
use Yajra\Datatables\Request;
16
17
/**
18
 * Class QueryBuilderEngine.
19
 *
20
 * @package Yajra\Datatables\Engines
21
 * @author  Arjay Angeles <[email protected]>
22
 */
23
class QueryBuilderEngine extends BaseEngine
24
{
25
    /**
26
     * @param \Illuminate\Database\Query\Builder $builder
27
     * @param \Yajra\Datatables\Request $request
28
     */
29
    public function __construct(Builder $builder, Request $request)
30
    {
31
        $this->query = $builder;
32
        $this->init($request, $builder);
33
    }
34
35
    /**
36
     * Initialize attributes.
37
     *
38
     * @param  \Yajra\Datatables\Request $request
39
     * @param  \Illuminate\Database\Query\Builder $builder
40
     * @param  string $type
41
     */
42
    protected function init($request, $builder, $type = 'builder')
43
    {
44
        $this->request    = $request;
45
        $this->query_type = $type;
46
        $this->columns    = $builder->columns;
47
        $this->connection = $builder->getConnection();
48
        $this->prefix     = $this->connection->getTablePrefix();
49
        $this->database   = $this->connection->getDriverName();
50
        if ($this->isDebugging()) {
51
            $this->connection->enableQueryLog();
52
        }
53
    }
54
55
    /**
56
     * Set auto filter off and run your own filter.
57
     * Overrides global search
58
     *
59
     * @param \Closure $callback
60
     * @param bool $globalSearch
61
     * @return $this
62
     */
63
    public function filter(Closure $callback, $globalSearch = false)
64
    {
65
        $this->overrideGlobalSearch($callback, $this->query, $globalSearch);
66
67
        return $this;
68
    }
69
70
    /**
71
     * Organizes works
72
     *
73
     * @param bool $mDataSupport
74
     * @param bool $orderFirst
75
     * @return \Illuminate\Http\JsonResponse
76
     */
77
    public function make($mDataSupport = false, $orderFirst = false)
78
    {
79
        return parent::make($mDataSupport, $orderFirst);
80
    }
81
82
    /**
83
     * Count total items.
84
     *
85
     * @return integer
86
     */
87
    public function totalCount()
88
    {
89
        return $this->totalRecords ? $this->totalRecords : $this->count();
90
    }
91
92
    /**
93
     * Counts current query.
94
     *
95
     * @return int
96
     */
97
    public function count()
98
    {
99
        $myQuery = clone $this->query;
100
        // if its a normal query ( no union, having and distinct word )
101
        // replace the select with static text to improve performance
102 View Code Duplication
        if (! Str::contains(Str::lower($myQuery->toSql()), ['union', 'having', 'distinct', 'order by', 'group by'])) {
0 ignored issues
show
Bug introduced by
The method toSql does only exist in Illuminate\Database\Query\Builder, but not in Illuminate\Database\Eloquent\Builder.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
103
            $row_count = $this->wrap('row_count');
104
            $myQuery->select($this->connection->raw("'1' as {$row_count}"));
0 ignored issues
show
Bug introduced by
The method select does only exist in Illuminate\Database\Query\Builder, but not in Illuminate\Database\Eloquent\Builder.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
105
        }
106
107
        return $this->connection->table($this->connection->raw('(' . $myQuery->toSql() . ') count_row_table'))
108
                                ->setBindings($myQuery->getBindings())->count();
0 ignored issues
show
Bug introduced by
The method getBindings does only exist in Illuminate\Database\Query\Builder, but not in Illuminate\Database\Eloquent\Builder.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
109
    }
110
111
    /**
112
     * Wrap column with DB grammar.
113
     *
114
     * @param string $column
115
     * @return string
116
     */
117
    protected function wrap($column)
118
    {
119
        return $this->connection->getQueryGrammar()->wrap($column);
120
    }
121
122
    /**
123
     * Perform global search.
124
     *
125
     * @return void
126
     */
127
    public function filtering()
128
    {
129
        $keyword = $this->request->keyword();
130
131
        if ($this->isSmartSearch()) {
132
            $this->smartGlobalSearch($keyword);
0 ignored issues
show
Bug introduced by
It seems like $keyword defined by $this->request->keyword() on line 129 can also be of type array; however, Yajra\Datatables\Engines...ne::smartGlobalSearch() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
133
134
            return;
135
        }
136
137
        $this->globalSearch($keyword);
0 ignored issues
show
Bug introduced by
It seems like $keyword defined by $this->request->keyword() on line 129 can also be of type array; however, Yajra\Datatables\Engines...rEngine::globalSearch() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
138
    }
139
140
    /**
141
     * Perform multi-term search by splitting keyword into
142
     * individual words and searches for each of them.
143
     *
144
     * @param string $keyword
145
     */
146
    private function smartGlobalSearch($keyword)
147
    {
148
        $keywords = array_filter(explode(' ', $keyword));
149
150
        foreach ($keywords as $keyword) {
151
            $this->globalSearch($keyword);
152
        }
153
    }
154
155
    /**
156
     * Perform global search for the given keyword.
157
     *
158
     * @param string $keyword
159
     */
160
    private function globalSearch($keyword)
161
    {
162
        $this->query->where(
163
            function ($query) use ($keyword) {
164
                $queryBuilder = $this->getQueryBuilder($query);
165
166
                foreach ($this->request->searchableColumnIndex() as $index) {
167
                    $columnName = $this->getColumnName($index);
168
                    if ($this->isBlacklisted($columnName) && ! $this->hasCustomFilter($columnName)) {
169
                        continue;
170
                    }
171
172
                    // check if custom column filtering is applied
173
                    if ($this->hasCustomFilter($columnName)) {
174
                        $columnDef = $this->columnDef['filter'][$columnName];
175
                        // check if global search should be applied for the specific column
176
                        $applyGlobalSearch = count($columnDef['parameters']) == 0 || end($columnDef['parameters']) !== false;
177
                        if (! $applyGlobalSearch) {
178
                            continue;
179
                        }
180
181 View Code Duplication
                        if ($columnDef['method'] instanceof Closure) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
182
                            $whereQuery = $queryBuilder->newQuery();
183
                            call_user_func_array($columnDef['method'], [$whereQuery, $keyword]);
184
                            $queryBuilder->addNestedWhereQuery($whereQuery, 'or');
185
                        } else {
186
                            $this->compileColumnQuery(
187
                                $queryBuilder,
188
                                Helper::getOrMethod($columnDef['method']),
189
                                $columnDef['parameters'],
190
                                $columnName,
191
                                $keyword
192
                            );
193
                        }
194 View Code Duplication
                    } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
195
                        if (count(explode('.', $columnName)) > 1) {
196
                            $eagerLoads     = $this->getEagerLoads();
197
                            $parts          = explode('.', $columnName);
198
                            $relationColumn = array_pop($parts);
199
                            $relation       = implode('.', $parts);
200
                            if (in_array($relation, $eagerLoads)) {
201
                                $this->compileRelationSearch(
202
                                    $queryBuilder,
203
                                    $relation,
204
                                    $relationColumn,
205
                                    $keyword
206
                                );
207
                            } else {
208
                                $this->compileQuerySearch($queryBuilder, $columnName, $keyword);
209
                            }
210
                        } else {
211
                            $this->compileQuerySearch($queryBuilder, $columnName, $keyword);
212
                        }
213
                    }
214
215
                    $this->isFilterApplied = true;
216
                }
217
            }
218
        );
219
    }
220
221
    /**
222
     * Check if column has custom filter handler.
223
     *
224
     * @param  string $columnName
225
     * @return bool
226
     */
227
    public function hasCustomFilter($columnName)
228
    {
229
        return isset($this->columnDef['filter'][$columnName]);
230
    }
231
232
    /**
233
     * Perform filter column on selected field.
234
     *
235
     * @param mixed $query
236
     * @param string|Closure $method
237
     * @param mixed $parameters
238
     * @param string $column
239
     * @param string $keyword
240
     */
241
    protected function compileColumnQuery($query, $method, $parameters, $column, $keyword)
242
    {
243
        if (method_exists($query, $method)
244
            && count($parameters) <= with(new \ReflectionMethod($query, $method))->getNumberOfParameters()
245
        ) {
246
            if (Str::contains(Str::lower($method), 'raw')
0 ignored issues
show
Bug introduced by
It seems like $method defined by parameter $method on line 241 can also be of type object<Closure>; however, Illuminate\Support\Str::lower() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
247
                || Str::contains(Str::lower($method), 'exists')
0 ignored issues
show
Bug introduced by
It seems like $method defined by parameter $method on line 241 can also be of type object<Closure>; however, Illuminate\Support\Str::lower() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
248
            ) {
249
                call_user_func_array(
250
                    [$query, $method],
251
                    $this->parameterize($parameters, $keyword)
252
                );
253
            } else {
254
                call_user_func_array(
255
                    [$query, $method],
256
                    $this->parameterize($column, $parameters, $keyword)
257
                );
258
            }
259
        }
260
    }
261
262
    /**
263
     * Build Query Builder Parameters.
264
     *
265
     * @return array
266
     */
267
    protected function parameterize()
268
    {
269
        $args       = func_get_args();
270
        $keyword    = count($args) > 2 ? $args[2] : $args[1];
271
        $parameters = Helper::buildParameters($args);
272
        $parameters = Helper::replacePatternWithKeyword($parameters, $keyword, '$1');
273
274
        return $parameters;
275
    }
276
277
    /**
278
     * Get eager loads keys if eloquent.
279
     *
280
     * @return array
281
     */
282
    protected function getEagerLoads()
283
    {
284
        if ($this->query_type == 'eloquent') {
285
            return array_keys($this->query->getEagerLoads());
0 ignored issues
show
Bug introduced by
The method getEagerLoads does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Query\Builder.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
286
        }
287
288
        return [];
289
    }
290
291
    /**
292
     * Add relation query on global search.
293
     *
294
     * @param mixed $query
295
     * @param string $relation
296
     * @param string $column
297
     * @param string $keyword
298
     */
299
    protected function compileRelationSearch($query, $relation, $column, $keyword)
300
    {
301
        $myQuery = clone $this->query;
302
303
        /**
304
         * For compile nested relation, we need store all nested relation as array
305
         * and reverse order to apply where query.
306
         * With this method we can create nested sub query with properly relation.
307
         */
308
309
        /**
310
         * Store all relation data that require in next step
311
         */
312
        $relationChunk = [];
313
314
        /**
315
         * Store last eloquent query builder for get next relation.
316
         */
317
        $lastQuery = $query;
318
319
        $relations    = explode('.', $relation);
320
        $lastRelation = end($relations);
321
        foreach ($relations as $relation) {
322
            $relationType = $myQuery->getModel()->{$relation}();
323
            $myQuery->orWhereHas($relation, function ($builder) use (
324
                $column,
325
                $keyword,
326
                $query,
327
                $relationType,
328
                $relation,
329
                $lastRelation,
330
                &$relationChunk,
331
                &$lastQuery
332
            ) {
333
                $builder->select($this->connection->raw('count(1)'));
334
335
                // We will perform search on last relation only.
336
                if ($relation == $lastRelation) {
337
                    $this->compileQuerySearch($builder, $column, $keyword, '');
338
                }
339
340
                // Put require object to next step!!
341
                $relationChunk[$relation] = [
342
                    'builder'      => $builder,
343
                    'relationType' => $relationType,
344
                    'query'        => $lastQuery,
345
                ];
346
347
                // This is trick make sub query.
348
                $lastQuery = $builder;
349
            });
350
351
            // This is trick to make nested relation by pass previous relation to be next query eloquent builder
352
            $myQuery = $relationType;
353
        }
354
355
        /**
356
         * Reverse them all
357
         */
358
        $relationChunk = array_reverse($relationChunk, true);
359
360
        /**
361
         * Create valuable for use in check last relation
362
         */
363
        end($relationChunk);
364
        $lastRelation = key($relationChunk);
365
        reset($relationChunk);
366
367
        /**
368
         * Walking ...
369
         */
370
        foreach ($relationChunk as $relation => $chunk) {
371
            // Prepare variables
372
            $builder  = $chunk['builder'];
373
            $query    = $chunk['query'];
374
            $bindings = $builder->getBindings();
375
            $builder  = "({$builder->toSql()}) >= 1";
376
377
            // Check if it last relation we will use orWhereRaw
378
            if ($lastRelation == $relation) {
379
                $relationMethod = "orWhereRaw";
380
            } else {
381
                // For case parent relation of nested relation.
382
                // We must use and for properly query and get correct result
383
                $relationMethod = "whereRaw";
384
            }
385
386
            $query->{$relationMethod}($builder, $bindings);
387
        }
388
    }
389
390
    /**
391
     * Compile query builder where clause depending on configurations.
392
     *
393
     * @param mixed $query
394
     * @param string $column
395
     * @param string $keyword
396
     * @param string $relation
397
     */
398
    protected function compileQuerySearch($query, $column, $keyword, $relation = 'or')
399
    {
400
        $column = $this->addTablePrefix($query, $column);
401
        $column = $this->castColumn($column);
402
        $sql    = $column . ' LIKE ?';
403
404
        if ($this->isCaseInsensitive()) {
405
            $sql = 'LOWER(' . $column . ') LIKE ?';
406
        }
407
408
        $query->{$relation . 'WhereRaw'}($sql, [$this->prepareKeyword($keyword)]);
409
    }
410
411
    /**
412
     * Patch for fix about ambiguous field.
413
     * Ambiguous field error will appear when query use join table and search with keyword.
414
     *
415
     * @param mixed $query
416
     * @param string $column
417
     * @return string
418
     */
419
    protected function addTablePrefix($query, $column)
420
    {
421
        // Check if field does not have a table prefix
422
        if (strpos($column, '.') === false) {
423
            // Alternative method to check instanceof \Illuminate\Database\Eloquent\Builder
424
            if (method_exists($query, 'getQuery')) {
425
                $q = $query->getQuery();
426
            } else {
427
                $q = $query;
428
            }
429
430
            if (! $q->from instanceof Expression) {
431
                // Get table from query and add it.
432
                $column = $q->from . '.' . $column;
433
            }
434
        }
435
436
        return $this->wrap($column);
437
    }
438
439
    /**
440
     * Wrap a column and cast in pgsql.
441
     *
442
     * @param  string $column
443
     * @return string
444
     */
445
    protected function castColumn($column)
446
    {
447
        if ($this->database === 'pgsql') {
448
            $column = 'CAST(' . $column . ' as TEXT)';
449
        } elseif ($this->database === 'firebird') {
450
            $column = 'CAST(' . $column . ' as VARCHAR(255))';
451
        }
452
453
        return $column;
454
    }
455
456
    /**
457
     * Prepare search keyword based on configurations.
458
     *
459
     * @param string $keyword
460
     * @return string
461
     */
462
    protected function prepareKeyword($keyword)
463
    {
464
        if ($this->isCaseInsensitive()) {
465
            $keyword = Str::lower($keyword);
466
        }
467
468
        if ($this->isWildcard()) {
469
            $keyword = $this->wildcardLikeString($keyword);
470
        }
471
472
        if ($this->isSmartSearch()) {
473
            $keyword = "%$keyword%";
474
        }
475
476
        return $keyword;
477
    }
478
479
    /**
480
     * Perform column search.
481
     *
482
     * @return void
483
     */
484
    public function columnSearch()
485
    {
486
        $columns = $this->request->columns();
487
488
        foreach ($columns as $index => $column) {
489
            if (! $this->request->isColumnSearchable($index)) {
490
                continue;
491
            }
492
493
            $column = $this->getColumnName($index);
494
495
            if (isset($this->columnDef['filter'][$column])) {
496
                $columnDef = $this->columnDef['filter'][$column];
497
                // get a raw keyword (without wildcards)
498
                $keyword = $this->getSearchKeyword($index, true);
499
                $builder = $this->getQueryBuilder();
500
501 View Code Duplication
                if ($columnDef['method'] instanceof Closure) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
502
                    $whereQuery = $builder->newQuery();
503
                    call_user_func_array($columnDef['method'], [$whereQuery, $keyword]);
504
                    $builder->addNestedWhereQuery($whereQuery);
505
                } else {
506
                    $this->compileColumnQuery(
507
                        $builder,
508
                        $columnDef['method'],
509
                        $columnDef['parameters'],
510
                        $column,
511
                        $keyword
0 ignored issues
show
Bug introduced by
It seems like $keyword defined by $this->getSearchKeyword($index, true) on line 498 can also be of type array; however, Yajra\Datatables\Engines...e::compileColumnQuery() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
512
                    );
513
                }
514 View Code Duplication
            } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
515
                if (count(explode('.', $column)) > 1) {
516
                    $eagerLoads     = $this->getEagerLoads();
517
                    $parts          = explode('.', $column);
518
                    $relationColumn = array_pop($parts);
519
                    $relation       = implode('.', $parts);
520
                    if (in_array($relation, $eagerLoads)) {
521
                        $column = $this->joinEagerLoadedColumn($relation, $relationColumn);
522
                    }
523
                }
524
525
                $keyword = $this->getSearchKeyword($index);
526
                $this->compileColumnSearch($index, $column, $keyword);
0 ignored issues
show
Bug introduced by
It seems like $keyword defined by $this->getSearchKeyword($index) on line 525 can also be of type array; however, Yajra\Datatables\Engines...::compileColumnSearch() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
527
            }
528
529
            $this->isFilterApplied = true;
530
        }
531
    }
532
533
    /**
534
     * Get proper keyword to use for search.
535
     *
536
     * @param int $i
537
     * @param bool $raw
538
     * @return string
539
     */
540
    protected function getSearchKeyword($i, $raw = false)
541
    {
542
        $keyword = $this->request->columnKeyword($i);
543
        if ($raw || $this->request->isRegex($i)) {
544
            return $keyword;
545
        }
546
547
        return $this->setupKeyword($keyword);
0 ignored issues
show
Bug introduced by
It seems like $keyword defined by $this->request->columnKeyword($i) on line 542 can also be of type array; however, Yajra\Datatables\Engines...eEngine::setupKeyword() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
548
    }
549
550
    /**
551
     * Join eager loaded relation and get the related column name.
552
     *
553
     * @param string $relation
554
     * @param string $relationColumn
555
     * @return string
556
     */
557
    protected function joinEagerLoadedColumn($relation, $relationColumn)
558
    {
559
        $lastQuery = $this->query;
560
        foreach (explode('.', $relation) as $eachRelation) {
561
            $model = $lastQuery->getRelation($eachRelation);
562
            switch (true) {
563
                case $model instanceof BelongsToMany:
564
                    $pivot   = $model->getTable();
565
                    $pivotPK = $model->getExistenceCompareKey();
566
                    $pivotFK = $model->getQualifiedParentKeyName();
567
                    $this->performJoin($pivot, $pivotPK, $pivotFK);
568
569
                    $related = $model->getRelated();
570
                    $table   = $related->getTable();
571
                    $tablePK = $related->getForeignKey();
572
                    $foreign = $pivot . '.' . $tablePK;
573
                    $other   = $related->getQualifiedKeyName();
574
575
                    $lastQuery->addSelect($table . '.' . $relationColumn);
576
                    $this->performJoin($table, $foreign, $other);
577
578
                    break;
579
580
                case $model instanceof HasOneOrMany:
581
                    $table   = $model->getRelated()->getTable();
582
                    $foreign = $model->getQualifiedForeignKeyName();
583
                    $other   = $model->getQualifiedParentKeyName();
584
                    break;
585
586
                case $model instanceof BelongsTo:
587
                    $table   = $model->getRelated()->getTable();
588
                    $foreign = $model->getQualifiedForeignKey();
589
                    $other   = $model->getQualifiedOwnerKeyName();
590
                    break;
591
592
                default:
593
                    $table = $model->getRelated()->getTable();
594
                    if ($model instanceof HasOneOrMany) {
595
                        $foreign = $model->getForeignKey();
0 ignored issues
show
Bug introduced by
The method getForeignKey() does not exist on Illuminate\Database\Eloq...\Relations\HasOneOrMany. Did you maybe mean getForeignKeyName()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
596
                        $other   = $model->getQualifiedParentKeyName();
597
                    } else {
598
                        $foreign = $model->getQualifiedForeignKey();
599
                        $other   = $model->getQualifiedOtherKeyName();
600
                    }
601
            }
602
            $this->performJoin($table, $foreign, $other);
603
            $lastQuery = $model->getQuery();
604
        }
605
606
        return $table . '.' . $relationColumn;
0 ignored issues
show
Bug introduced by
The variable $table does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
607
    }
608
609
    /**
610
     * Perform join query.
611
     *
612
     * @param string $table
613
     * @param string $foreign
614
     * @param string $other
615
     */
616
    protected function performJoin($table, $foreign, $other)
617
    {
618
        $joins = [];
619
        foreach ((array) $this->getQueryBuilder()->joins as $key => $join) {
620
            $joins[] = $join->table;
621
        }
622
623
        if (! in_array($table, $joins)) {
624
            $this->getQueryBuilder()->leftJoin($table, $foreign, '=', $other);
625
        }
626
    }
627
628
    /**
629
     * Compile queries for column search.
630
     *
631
     * @param int $i
632
     * @param mixed $column
633
     * @param string $keyword
634
     */
635
    protected function compileColumnSearch($i, $column, $keyword)
636
    {
637
        if ($this->request->isRegex($i)) {
638
            $column = strstr($column, '(') ? $this->connection->raw($column) : $column;
639
            $this->regexColumnSearch($column, $keyword);
640
        } else {
641
            $this->compileQuerySearch($this->query, $column, $keyword, '');
642
        }
643
    }
644
645
    /**
646
     * Compile regex query column search.
647
     *
648
     * @param mixed $column
649
     * @param string $keyword
650
     */
651
    protected function regexColumnSearch($column, $keyword)
652
    {
653
        if ($this->isOracleSql()) {
654
            $sql = ! $this->isCaseInsensitive() ? 'REGEXP_LIKE( ' . $column . ' , ? )' : 'REGEXP_LIKE( LOWER(' . $column . ') , ?, \'i\' )';
655
            $this->query->whereRaw($sql, [$keyword]);
0 ignored issues
show
Bug introduced by
The method whereRaw does only exist in Illuminate\Database\Query\Builder, but not in Illuminate\Database\Eloquent\Builder.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
656
        } elseif ($this->database == 'pgsql') {
657
            $sql = ! $this->isCaseInsensitive() ? $column . ' ~ ?' : $column . ' ~* ? ';
658
            $this->query->whereRaw($sql, [$keyword]);
659
        } else {
660
            $sql = ! $this->isCaseInsensitive() ? $column . ' REGEXP ?' : 'LOWER(' . $column . ') REGEXP ?';
661
            $this->query->whereRaw($sql, [Str::lower($keyword)]);
662
        }
663
    }
664
665
    /**
666
     * Perform sorting of columns.
667
     *
668
     * @return void
669
     */
670
    public function ordering()
671
    {
672
        if ($this->orderCallback) {
673
            call_user_func($this->orderCallback, $this->getQueryBuilder());
674
675
            return;
676
        }
677
678
        foreach ($this->request->orderableColumns() as $orderable) {
679
            $column = $this->getColumnName($orderable['column'], true);
680
681
            if ($this->isBlacklisted($column) && ! $this->hasCustomOrder($column)) {
682
                continue;
683
            }
684
685
            if ($this->hasCustomOrder($column)) {
686
                $method     = $this->columnDef['order'][$column]['method'];
687
                $parameters = $this->columnDef['order'][$column]['parameters'];
688
                $this->compileColumnQuery(
689
                    $this->getQueryBuilder(),
690
                    $method,
691
                    $parameters,
692
                    $column,
693
                    $orderable['direction']
694
                );
695
            } else {
696
                $valid = 1;
697
                if (count(explode('.', $column)) > 1) {
698
                    $eagerLoads     = $this->getEagerLoads();
699
                    $parts          = explode('.', $column);
700
                    $relationColumn = array_pop($parts);
701
                    $relation       = implode('.', $parts);
702
703
                    if (in_array($relation, $eagerLoads)) {
704
                        // Loop for nested relations
705
                        // This code is check morph many or not.
706
                        // If one of nested relation is MorphToMany
707
                        // we will call joinEagerLoadedColumn.
708
                        $lastQuery     = $this->query;
709
                        $isMorphToMany = false;
710
                        foreach (explode('.', $relation) as $eachRelation) {
711
                            $relationship = $lastQuery->getRelation($eachRelation);
712
                            if (! ($relationship instanceof MorphToMany)) {
713
                                $isMorphToMany = true;
714
                            }
715
                            $lastQuery = $relationship;
716
                        }
717
                        if ($isMorphToMany) {
718
                            $column = $this->joinEagerLoadedColumn($relation, $relationColumn);
719
                        } else {
720
                            $valid = 0;
721
                        }
722
                    }
723
                }
724
725
                if ($valid == 1) {
726
                    if ($this->nullsLast) {
727
                        $this->getQueryBuilder()->orderByRaw($this->getNullsLastSql($column, $orderable['direction']));
728
                    } else {
729
                        $this->getQueryBuilder()->orderBy($column, $orderable['direction']);
730
                    }
731
                }
732
            }
733
        }
734
    }
735
736
    /**
737
     * Check if column has custom sort handler.
738
     *
739
     * @param string $column
740
     * @return bool
741
     */
742
    protected function hasCustomOrder($column)
743
    {
744
        return isset($this->columnDef['order'][$column]);
745
    }
746
747
    /**
748
     * Get NULLS LAST SQL.
749
     *
750
     * @param  string $column
751
     * @param  string $direction
752
     * @return string
753
     */
754
    protected function getNullsLastSql($column, $direction)
755
    {
756
        $sql = Config::get('datatables.nulls_last_sql', '%s %s NULLS LAST');
757
758
        return sprintf($sql, $column, $direction);
759
    }
760
761
    /**
762
     * Perform pagination
763
     *
764
     * @return void
765
     */
766
    public function paging()
767
    {
768
        $this->query->skip($this->request->input('start'))
0 ignored issues
show
Bug introduced by
The method skip does only exist in Illuminate\Database\Query\Builder, but not in Illuminate\Database\Eloquent\Builder.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
769
                    ->take((int) $this->request->input('length') > 0 ? $this->request->input('length') : 10);
770
    }
771
772
    /**
773
     * Get results
774
     *
775
     * @return array|static[]
776
     */
777
    public function results()
778
    {
779
        return $this->query->get();
780
    }
781
782
    /**
783
     * Add column in collection.
784
     *
785
     * @param string $name
786
     * @param string|callable $content
787
     * @param bool|int $order
788
     * @return \Yajra\Datatables\Engines\BaseEngine|\Yajra\Datatables\Engines\QueryBuilderEngine
789
     */
790
    public function addColumn($name, $content, $order = false)
791
    {
792
        $this->pushToBlacklist($name);
793
794
        return parent::addColumn($name, $content, $order);
795
    }
796
}
797