Completed
Push — master ( f28daa...aab2b5 )
by Arjay
02:12
created

QueryBuilderEngine::resolveOrderByColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Yajra\Datatables\Engines;
4
5
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
6
use Illuminate\Database\Query\Builder;
7
use Illuminate\Database\Query\Expression;
8
use Illuminate\Support\Str;
9
10
/**
11
 * Class QueryBuilderEngine.
12
 *
13
 * @package Yajra\Datatables\Engines
14
 * @author  Arjay Angeles <[email protected]>
15
 */
16
class QueryBuilderEngine extends BaseEngine
17
{
18
    /**
19
     * Builder object.
20
     *
21
     * @var \Illuminate\Database\Query\Builder
22
     */
23
    protected $query;
24
25
    /**
26
     * Database connection used.
27
     *
28
     * @var \Illuminate\Database\Connection
29
     */
30
    protected $connection;
31
32
    /**
33
     * @param \Illuminate\Database\Query\Builder $builder
34
     */
35
    public function __construct(Builder $builder)
36
    {
37
        $this->query      = $builder;
38
        $this->request    = resolve('datatables.request');
39
        $this->config     = resolve('datatables.config');
40
        $this->columns    = $builder->columns;
41
        $this->connection = $builder->getConnection();
42
        if ($this->config->isDebugging()) {
43
            $this->connection->enableQueryLog();
44
        }
45
    }
46
47
    /**
48
     * Set auto filter off and run your own filter.
49
     * Overrides global search.
50
     *
51
     * @param callable $callback
52
     * @param bool     $globalSearch
53
     * @return $this
54
     */
55
    public function filter(callable $callback, $globalSearch = false)
56
    {
57
        $this->overrideGlobalSearch($callback, $this->query, $globalSearch);
58
59
        return $this;
60
    }
61
62
    /**
63
     * Organizes works.
64
     *
65
     * @param bool $mDataSupport
66
     * @return \Illuminate\Http\JsonResponse
67
     * @throws \Exception
68
     */
69
    public function make($mDataSupport = false)
70
    {
71
        try {
72
            $this->totalRecords = $this->totalCount();
73
74
            if ($this->totalRecords) {
75
                $this->filterRecords();
76
                $this->ordering();
77
                $this->paginate();
78
            }
79
80
            $data = $this->transform($this->getProcessedData($mDataSupport));
81
82
            return $this->render($data);
83
        } catch (\Exception $exception) {
84
            return $this->errorResponse($exception);
85
        }
86
    }
87
88
    /**
89
     * Count total items.
90
     *
91
     * @return integer
92
     */
93
    public function totalCount()
94
    {
95
        return $this->totalRecords ? $this->totalRecords : $this->count();
96
    }
97
98
    /**
99
     * Counts current query.
100
     *
101
     * @return int
102
     */
103
    public function count()
104
    {
105
        $builder = $this->prepareCountQuery();
106
        $table   = $this->connection->raw('(' . $builder->toSql() . ') count_row_table');
107
108
        return $this->connection->table($table)
109
                                ->setBindings($builder->getBindings())
110
                                ->count();
111
    }
112
113
    /**
114
     * Prepare count query builder.
115
     *
116
     * @return \Illuminate\Database\Query\Builder
117
     */
118
    protected function prepareCountQuery()
119
    {
120
        $builder = clone $this->query;
121
122
        if ($this->isComplexQuery($builder)) {
123
            $row_count = $this->wrap('row_count');
124
            $builder->select($this->connection->raw("'1' as {$row_count}"));
125
        }
126
127
        return $builder;
128
    }
129
130
    /**
131
     * Check if builder query uses complex sql.
132
     *
133
     * @param \Illuminate\Database\Query\Builder $builder
134
     * @return bool
135
     */
136
    protected function isComplexQuery($builder)
137
    {
138
        return !Str::contains(Str::lower($builder->toSql()), ['union', 'having', 'distinct', 'order by', 'group by']);
139
    }
140
141
    /**
142
     * Wrap column with DB grammar.
143
     *
144
     * @param string $column
145
     * @return string
146
     */
147
    protected function wrap($column)
148
    {
149
        return $this->connection->getQueryGrammar()->wrap($column);
150
    }
151
152
    /**
153
     * Perform sorting of columns.
154
     *
155
     * @return void
156
     */
157
    public function ordering()
158
    {
159
        if ($this->orderCallback) {
160
            call_user_func($this->orderCallback, $this->getBaseQueryBuilder());
161
162
            return;
163
        }
164
165
        foreach ($this->request->orderableColumns() as $orderable) {
166
            $column = $this->getColumnName($orderable['column'], true);
167
168
            if ($this->isBlacklisted($column) && !$this->hasCustomOrder($column)) {
169
                continue;
170
            }
171
172
            if ($this->hasCustomOrder($column)) {
173
                $this->applyOrderColumn($column, $orderable);
174
                continue;
175
            }
176
177
            $column = $this->resolveOrderByColumn($column);
178
            if ($this->nullsLast) {
179
                $this->getBaseQueryBuilder()->orderByRaw($this->getNullsLastSql($column, $orderable['direction']));
180
            } else {
181
                $this->getBaseQueryBuilder()->orderBy($column, $orderable['direction']);
182
            }
183
        }
184
    }
185
186
    /**
187
     * Get the base query builder instance.
188
     *
189
     * @param mixed $instance
190
     * @return \Illuminate\Database\Query\Builder
191
     */
192
    protected function getBaseQueryBuilder($instance = null)
193
    {
194
        if (!$instance) {
195
            $instance = $this->query;
196
        }
197
198
        if ($instance instanceof EloquentBuilder) {
199
            return $instance->getQuery();
200
        }
201
202
        return $instance;
203
    }
204
205
    /**
206
     * Check if column has custom sort handler.
207
     *
208
     * @param string $column
209
     * @return bool
210
     */
211
    protected function hasCustomOrder($column)
212
    {
213
        return isset($this->columnDef['order'][$column]);
214
    }
215
216
    /**
217
     * Apply orderColumn custom query.
218
     *
219
     * @param string $column
220
     * @param array  $orderable
221
     */
222
    protected function applyOrderColumn($column, $orderable): void
223
    {
224
        $sql      = $this->columnDef['order'][$column]['sql'];
225
        $sql      = str_replace('$1', $orderable['direction'], $sql);
226
        $bindings = $this->columnDef['order'][$column]['bindings'];
227
        $this->query->orderByRaw($sql, $bindings);
228
    }
229
230
    /**
231
     * Resolve the proper column name be used.
232
     *
233
     * @param string $column
234
     * @return string
235
     */
236
    protected function resolveOrderByColumn($column)
237
    {
238
        return $column;
239
    }
240
241
    /**
242
     * Get NULLS LAST SQL.
243
     *
244
     * @param  string $column
245
     * @param  string $direction
246
     * @return string
247
     */
248
    protected function getNullsLastSql($column, $direction)
249
    {
250
        $sql = $this->config->get('datatables.nulls_last_sql', '%s %s NULLS LAST');
251
252
        return sprintf($sql, $column, $direction);
253
    }
254
255
    /**
256
     * Perform column search.
257
     *
258
     * @return void
259
     */
260
    public function columnSearch()
261
    {
262
        $columns = $this->request->columns();
263
264
        foreach ($columns as $index => $column) {
265
            if (!$this->request->isColumnSearchable($index)) {
266
                continue;
267
            }
268
269
            $column = $this->getColumnName($index);
270
271
            if ($this->hasCustomFilter($column)) {
272
                $keyword = $this->getColumnSearchKeyword($index, $raw = true);
273
                $this->applyFilterColumn($this->query, $column, $keyword);
274
            } else {
275
                if (count(explode('.', $column)) > 1) {
276
                    $eagerLoads     = $this->getEagerLoads();
277
                    $parts          = explode('.', $column);
278
                    $relationColumn = array_pop($parts);
279
                    $relation       = implode('.', $parts);
280
                    if (in_array($relation, $eagerLoads)) {
281
                        $column = $this->joinEagerLoadedColumn($relation, $relationColumn);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Yajra\Datatables\Engines\QueryBuilderEngine as the method joinEagerLoadedColumn() does only exist in the following sub-classes of Yajra\Datatables\Engines\QueryBuilderEngine: Yajra\Datatables\Engines\EloquentEngine. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
282
                    }
283
                }
284
285
                $keyword = $this->getColumnSearchKeyword($index);
286
                $this->compileColumnSearch($index, $column, $keyword);
287
            }
288
289
            $this->isFilterApplied = true;
290
        }
291
    }
292
293
    /**
294
     * Check if column has custom filter handler.
295
     *
296
     * @param  string $columnName
297
     * @return bool
298
     */
299
    public function hasCustomFilter($columnName)
300
    {
301
        return isset($this->columnDef['filter'][$columnName]);
302
    }
303
304
    /**
305
     * Get column keyword to use for search.
306
     *
307
     * @param int  $i
308
     * @param bool $raw
309
     * @return string
310
     */
311
    protected function getColumnSearchKeyword($i, $raw = false)
312
    {
313
        $keyword = $this->request->columnKeyword($i);
314
        if ($raw || $this->request->isRegex($i)) {
315
            return $keyword;
316
        }
317
318
        return $this->setupKeyword($keyword);
319
    }
320
321
    /**
322
     * Apply filterColumn api search.
323
     *
324
     * @param mixed  $query
325
     * @param string $columnName
326
     * @param string $keyword
327
     * @param string $boolean
328
     */
329
    protected function applyFilterColumn($query, $columnName, $keyword, $boolean = 'and')
330
    {
331
        $callback = $this->columnDef['filter'][$columnName]['method'];
332
        $builder  = $query->newQuery();
333
        $callback($builder, $keyword);
334
        $query->addNestedWhereQuery($builder, $boolean);
335
    }
336
337
    /**
338
     * Get eager loads keys if eloquent.
339
     *
340
     * @return array
341
     */
342
    protected function getEagerLoads()
343
    {
344
        if ($this->query instanceof EloquentBuilder) {
345
            return array_keys($this->query->getEagerLoads());
346
        }
347
348
        return [];
349
    }
350
351
    /**
352
     * Compile queries for column search.
353
     *
354
     * @param int    $i
355
     * @param string $column
356
     * @param string $keyword
357
     */
358
    protected function compileColumnSearch($i, $column, $keyword)
359
    {
360
        if ($this->request->isRegex($i)) {
361
            $column = strstr($column, '(') ? $this->connection->raw($column) : $column;
362
            $this->regexColumnSearch($column, $keyword);
363
        } else {
364
            $this->compileQuerySearch($this->query, $column, $keyword, '');
365
        }
366
    }
367
368
    /**
369
     * Compile regex query column search.
370
     *
371
     * @param mixed  $column
372
     * @param string $keyword
373
     */
374
    protected function regexColumnSearch($column, $keyword)
375
    {
376
        switch ($this->connection->getDriverName()) {
377
            case 'oracle':
378
                $sql = !$this->config
379
                    ->isCaseInsensitive() ? 'REGEXP_LIKE( ' . $column . ' , ? )' : 'REGEXP_LIKE( LOWER(' . $column . ') , ?, \'i\' )';
380
                break;
381
382
            case 'pgsql':
383
                $sql = !$this->config->isCaseInsensitive() ? $column . ' ~ ?' : $column . ' ~* ? ';
384
                break;
385
386
            default:
387
                $sql     = !$this->config
388
                    ->isCaseInsensitive() ? $column . ' REGEXP ?' : 'LOWER(' . $column . ') REGEXP ?';
389
                $keyword = Str::lower($keyword);
390
        }
391
392
        $this->query->whereRaw($sql, [$keyword]);
393
    }
394
395
    /**
396
     * Compile query builder where clause depending on configurations.
397
     *
398
     * @param mixed  $query
399
     * @param string $column
400
     * @param string $keyword
401
     * @param string $relation
402
     */
403
    protected function compileQuerySearch($query, $column, $keyword, $relation = 'or')
404
    {
405
        $column = $this->addTablePrefix($query, $column);
406
        $column = $this->castColumn($column);
407
        $sql    = $column . ' LIKE ?';
408
409
        if ($this->config->isCaseInsensitive()) {
410
            $sql = 'LOWER(' . $column . ') LIKE ?';
411
        }
412
413
        $query->{$relation . 'WhereRaw'}($sql, [$this->prepareKeyword($keyword)]);
414
    }
415
416
    /**
417
     * Patch for fix about ambiguous field.
418
     * Ambiguous field error will appear when query use join table and search with keyword.
419
     *
420
     * @param mixed  $query
421
     * @param string $column
422
     * @return string
423
     */
424
    protected function addTablePrefix($query, $column)
425
    {
426
        if (strpos($column, '.') === false) {
427
            $q = $this->getBaseQueryBuilder($query);
428
            if (!$q->from instanceof Expression) {
429
                $column = $q->from . '.' . $column;
430
            }
431
        }
432
433
        return $this->wrap($column);
434
    }
435
436
    /**
437
     * Wrap a column and cast based on database driver.
438
     *
439
     * @param  string $column
440
     * @return string
441
     */
442
    protected function castColumn($column)
443
    {
444
        switch ($this->connection->getDriverName()) {
445
            case 'pgsql':
446
                return 'CAST(' . $column . ' as TEXT)';
447
            case 'firebird':
448
                return 'CAST(' . $column . ' as VARCHAR(255))';
449
            default:
450
                return $column;
451
        }
452
    }
453
454
    /**
455
     * Prepare search keyword based on configurations.
456
     *
457
     * @param string $keyword
458
     * @return string
459
     */
460
    protected function prepareKeyword($keyword)
461
    {
462
        if ($this->config->isCaseInsensitive()) {
463
            $keyword = Str::lower($keyword);
464
        }
465
466
        if ($this->config->isWildcard()) {
467
            $keyword = $this->wildcardLikeString($keyword);
468
        }
469
470
        if ($this->config->isSmartSearch()) {
471
            $keyword = "%$keyword%";
472
        }
473
474
        return $keyword;
475
    }
476
477
    /**
478
     * Perform pagination.
479
     *
480
     * @return void
481
     */
482
    public function paging()
483
    {
484
        $this->query->skip($this->request->input('start'))
485
                    ->take((int) $this->request->input('length') > 0 ? $this->request->input('length') : 10);
486
    }
487
488
    /**
489
     * Get paginated results.
490
     *
491
     * @return \Illuminate\Support\Collection
492
     */
493
    public function results()
494
    {
495
        return $this->query->get();
496
    }
497
498
    /**
499
     * Add column in collection.
500
     *
501
     * @param string          $name
502
     * @param string|callable $content
503
     * @param bool|int        $order
504
     * @return \Yajra\Datatables\Engines\BaseEngine|\Yajra\Datatables\Engines\QueryBuilderEngine
505
     */
506
    public function addColumn($name, $content, $order = false)
507
    {
508
        $this->pushToBlacklist($name);
509
510
        return parent::addColumn($name, $content, $order);
511
    }
512
513
    /**
514
     * Get query builder instance.
515
     *
516
     * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder
517
     */
518
    public function getQuery()
519
    {
520
        return $this->query;
521
    }
522
523
    /**
524
     * Perform global search for the given keyword.
525
     *
526
     * @param string $keyword
527
     */
528
    protected function globalSearch($keyword)
529
    {
530
        $this->query->where(function ($query) use ($keyword) {
531
            $query = $this->getBaseQueryBuilder($query);
532
533
            collect($this->request->searchableColumnIndex())
534
                ->map(function ($index) {
535
                    return $this->getColumnName($index);
536
                })
537
                ->reject(function ($column) {
538
                    return $this->isBlacklisted($column) && !$this->hasCustomFilter($column);
539
                })
540
                ->each(function ($column) use ($keyword, $query) {
541
                    if ($this->hasCustomFilter($column)) {
542
                        $this->applyFilterColumn($query, $column, $keyword, 'or');
543
                    } else {
544
                        $this->compileQuerySearch($query, $column, $keyword);
545
                    }
546
547
                    $this->isFilterApplied = true;
548
                });
549
        });
550
    }
551
552
    /**
553
     * Append debug parameters on output.
554
     *
555
     * @param  array $output
556
     * @return array
557
     */
558
    protected function showDebugger(array $output)
559
    {
560
        $output['queries'] = $this->connection->getQueryLog();
561
        $output['input']   = $this->request->all();
562
563
        return $output;
564
    }
565
}
566