Completed
Branch 6.0 (a3f97d)
by Arjay
02:24
created

QueryBuilderEngine::getEagerLoads()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
1
<?php
2
3
namespace Yajra\Datatables\Engines;
4
5
/**
6
 * Laravel Datatables Query Builder Engine
7
 *
8
 * @package  Laravel
9
 * @category Package
10
 * @author   Arjay Angeles <[email protected]>
11
 */
12
13
use Closure;
14
use Illuminate\Database\Query\Builder;
15
use Illuminate\Support\Str;
16
use Yajra\Datatables\Contracts\DataTableEngineContract;
17
use Yajra\Datatables\Helper;
18
use Yajra\Datatables\Request;
19
20
class QueryBuilderEngine extends BaseEngine implements DataTableEngineContract
21
{
22
    /**
23
     * @param \Illuminate\Database\Query\Builder $builder
24
     * @param \Yajra\Datatables\Request $request
25
     */
26
    public function __construct(Builder $builder, Request $request)
27
    {
28
        $this->query = $builder;
29
        $this->init($request, $builder);
30
    }
31
32
    /**
33
     * Initialize attributes.
34
     *
35
     * @param  \Yajra\Datatables\Request $request
36
     * @param  \Illuminate\Database\Query\Builder $builder
37
     * @param  string $type
38
     */
39
    protected function init($request, $builder, $type = 'builder')
0 ignored issues
show
Bug introduced by
You have injected the Request via parameter $request. This is generally not recommended as there might be multiple instances during a request cycle (f.e. when using sub-requests). Instead, it is recommended to inject the RequestStack and retrieve the current request each time you need it via getCurrentRequest().
Loading history...
40
    {
41
        $this->request    = $request;
42
        $this->query_type = $type;
43
        $this->columns    = $builder->columns;
44
        $this->connection = $builder->getConnection();
45
        $this->prefix     = $this->connection->getTablePrefix();
46
        $this->database   = $this->connection->getDriverName();
47
        if ($this->isDebugging()) {
48
            $this->connection->enableQueryLog();
49
        }
50
    }
51
52
    /**
53
     * @inheritdoc
54
     */
55
    public function filter(Closure $callback)
56
    {
57
        $this->overrideGlobalSearch($callback, $this->query);
58
59
        return $this;
60
    }
61
62
    /**
63
     * @inheritdoc
64
     */
65
    public function make($mDataSupport = false, $orderFirst = false)
66
    {
67
        return parent::make($mDataSupport, $orderFirst);
68
    }
69
70
    /**
71
     * @inheritdoc
72
     */
73
    public function totalCount()
74
    {
75
        return $this->count();
76
    }
77
78
    /**
79
     * Counts current query.
80
     *
81
     * @return int
82
     */
83
    public function count()
84
    {
85
        $myQuery = clone $this->query;
86
        // if its a normal query ( no union, having and distinct word )
87
        // replace the select with static text to improve performance
88
        if (! Str::contains(Str::lower($myQuery->toSql()), ['union', 'having', 'distinct', 'order by', 'group by'])) {
89
            $row_count = $this->connection->getQueryGrammar()->wrap('row_count');
90
            $myQuery->select($this->connection->raw("'1' as {$row_count}"));
91
        }
92
93
        return $this->connection->table($this->connection->raw('(' . $myQuery->toSql() . ') count_row_table'))
94
                                ->setBindings($myQuery->getBindings())->count();
95
    }
96
97
    /**
98
     * Perform global search.
99
     *
100
     * @return void
101
     */
102
    public function filtering()
103
    {
104
        $eagerLoads = $this->getEagerLoads();
105
106
        $this->query->where(
107
            function ($query) use ($eagerLoads) {
108
                $keyword = $this->setupKeyword($this->request->keyword());
109
                foreach ($this->request->searchableColumnIndex() as $index) {
110
                    $columnName = $this->setupColumnName($index);
111
112
                    if (isset($this->columnDef['filter'][$columnName])) {
113
                        $method     = Helper::getOrMethod($this->columnDef['filter'][$columnName]['method']);
114
                        $parameters = $this->columnDef['filter'][$columnName]['parameters'];
115
                        $this->compileColumnQuery(
116
                            $this->getQueryBuilder($query),
117
                            $method,
118
                            $parameters,
119
                            $columnName,
120
                            $keyword
121
                        );
122
                    } else {
123
                        if (count(explode('.', $columnName)) > 1) {
124
                            $parts          = explode('.', $columnName);
125
                            $relationColumn = array_pop($parts);
126
                            $relation       = implode('.', $parts);
127
                            if (in_array($relation, $eagerLoads)) {
128
                                $this->compileRelationSearch(
129
                                    $this->getQueryBuilder($query),
130
                                    $relation,
131
                                    $relationColumn,
132
                                    $keyword
133
                                );
134
                            } else {
135
                                $this->compileGlobalSearch($this->getQueryBuilder($query), $columnName, $keyword);
136
                            }
137
                        } else {
138
                            $this->compileGlobalSearch($this->getQueryBuilder($query), $columnName, $keyword);
139
                        }
140
                    }
141
142
                    $this->isFilterApplied = true;
143
                }
144
            }
145
        );
146
    }
147
148
    /**
149
     * Get eager loads keys if eloquent.
150
     *
151
     * @return array
152
     */
153
    private function getEagerLoads()
154
    {
155
        if ($this->query_type == 'eloquent') {
156
            return array_keys($this->query->getEagerLoads());
157
        }
158
159
        return [];
160
    }
161
162
    /**
163
     * Perform filter column on selected field.
164
     *
165
     * @param mixed $query
166
     * @param string $method
167
     * @param mixed $parameters
168
     * @param string $column
169
     * @param string $keyword
170
     */
171
    protected function compileColumnQuery($query, $method, $parameters, $column, $keyword)
172
    {
173
        if (method_exists($query, $method)
174
            && count($parameters) <= with(new \ReflectionMethod($query, $method))->getNumberOfParameters()
175
        ) {
176
            if (Str::contains(Str::lower($method), 'raw')
177
                || Str::contains(Str::lower($method), 'exists')
178
            ) {
179
                call_user_func_array(
180
                    [$query, $method],
181
                    $this->parameterize($parameters, $keyword)
182
                );
183
            } else {
184
                call_user_func_array(
185
                    [$query, $method],
186
                    $this->parameterize($column, $parameters, $keyword)
187
                );
188
            }
189
        }
190
    }
191
192
    /**
193
     * Build Query Builder Parameters.
194
     *
195
     * @return array
196
     */
197
    protected function parameterize()
198
    {
199
        $args       = func_get_args();
200
        $keyword    = count($args) > 2 ? $args[2] : $args[1];
201
        $parameters = Helper::buildParameters($args);
202
        $parameters = Helper::replacePatternWithKeyword($parameters, $keyword, '$1');
203
204
        return $parameters;
205
    }
206
207
    /**
208
     * Add relation query on global search.
209
     *
210
     * @param mixed $query
211
     * @param string $relation
212
     * @param string $column
213
     * @param string $keyword
214
     */
215
    protected function compileRelationSearch($query, $relation, $column, $keyword)
216
    {
217
        $myQuery = clone $this->query;
218
        $myQuery->orWhereHas($relation, function ($q) use ($column, $keyword, $query) {
219
            $q->where($column, 'like', $keyword);
220
            $sql = $q->toSql();
221
            $sql = "($sql) >= 1";
222
            $query->orWhereRaw($sql, [$keyword]);
223
        });
224
    }
225
226
    /**
227
     * Add a query on global search.
228
     *
229
     * @param mixed $query
230
     * @param string $column
231
     * @param string $keyword
232
     */
233
    protected function compileGlobalSearch($query, $column, $keyword)
234
    {
235
        $column = $this->castColumn($column);
236
        $sql    = $column . ' LIKE ?';
237
        if ($this->isCaseInsensitive()) {
238
            $sql     = 'LOWER(' . $column . ') LIKE ?';
239
            $keyword = Str::lower($keyword);
240
        }
241
242
        $query->orWhereRaw($sql, [$keyword]);
243
    }
244
245
    /**
246
     * Wrap a column and cast in pgsql.
247
     *
248
     * @param  string $column
249
     * @return string
250
     */
251
    public function castColumn($column)
252
    {
253
        $column = $this->connection->getQueryGrammar()->wrap($column);
254
        if ($this->database === 'pgsql') {
255
            $column = 'CAST(' . $column . ' as TEXT)';
256
        }
257
258
        return $column;
259
    }
260
261
    /**
262
     * Perform column search.
263
     *
264
     * @return void
265
     */
266
    public function columnSearch()
267
    {
268
        $columns = $this->request->get('columns');
269
        for ($i = 0, $c = count($columns); $i < $c; $i++) {
270
            if ($this->request->isColumnSearchable($i)) {
271
                $column  = $this->setupColumnName($i);
272
                $keyword = $this->getSearchKeyword($i);
273
274
                if (isset($this->columnDef['filter'][$column])) {
275
                    $method     = $this->columnDef['filter'][$column]['method'];
276
                    $parameters = $this->columnDef['filter'][$column]['parameters'];
277
                    $this->compileColumnQuery($this->getQueryBuilder(), $method, $parameters, $column, $keyword);
278
                } else {
279
                    $column = $this->castColumn($column);
280
                    if ($this->isCaseInsensitive()) {
281
                        $this->caseInsensitiveColumnSearch($i, $column, $keyword);
282
                    } else {
283
                        $col = strstr($column, '(') ? $this->connection->raw($column) : $column;
284
                        $this->caseSensitiveColumnSearch($i, $col, $keyword);
0 ignored issues
show
Bug introduced by
It seems like $col defined by strstr($column, '(') ? $...>raw($column) : $column on line 283 can also be of type object<Illuminate\Database\Query\Expression>; however, Yajra\Datatables\Engines...SensitiveColumnSearch() 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...
285
                    }
286
                }
287
288
                $this->isFilterApplied = true;
289
            }
290
        }
291
    }
292
293
    /**
294
     * Get proper keyword to use for search.
295
     *
296
     * @param int $i
297
     * @return string
298
     */
299
    private function getSearchKeyword($i)
300
    {
301
        if ($this->request->isRegex($i)) {
302
            return $this->request->columnKeyword($i);
303
        }
304
305
        return $this->setupKeyword($this->request->columnKeyword($i));
306
    }
307
308
    /**
309
     * Perform case insensitive column search.
310
     *
311
     * @param int $i
312
     * @param string $column
313
     * @param string $keyword
314
     */
315
    protected function caseInsensitiveColumnSearch($i, $column, $keyword)
316
    {
317
        if ($this->request->isRegex($i)) {
318 View Code Duplication
            if ($this->isOracleSql()) {
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...
319
                $this->query->whereRaw('REGEXP_LIKE( LOWER(' . $column . ') , ?, \'i\' )', [$keyword]);
320
            } else {
321
                $this->query->whereRaw('LOWER(' . $column . ') REGEXP ?', [Str::lower($keyword)]);
322
            }
323
        } else {
324
            $this->query->whereRaw('LOWER(' . $column . ') LIKE ?', [Str::lower($keyword)]);
325
        }
326
    }
327
328
    /**
329
     * Perform case sensitive column search.
330
     *
331
     * @param int $i
332
     * @param string $column
333
     * @param string $keyword
334
     */
335
    protected function caseSensitiveColumnSearch($i, $column, $keyword)
336
    {
337
        if ($this->request->isRegex($i)) {
338 View Code Duplication
            if ($this->isOracleSql()) {
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...
339
                $this->query->whereRaw('REGEXP_LIKE( ' . $column . ' , ? )', [$keyword]);
340
            } else {
341
                $this->query->whereRaw($column . ' REGEXP ?', [$keyword]);
342
            }
343
        } else {
344
            $this->query->whereRaw($column . ' LIKE ?', [$keyword]);
345
        }
346
    }
347
348
    /**
349
     * @inheritdoc
350
     */
351
    public function ordering()
352
    {
353
        if ($this->orderCallback) {
354
            call_user_func($this->orderCallback, $this->getQueryBuilder());
355
356
            return;
357
        }
358
359
        foreach ($this->request->orderableColumns() as $orderable) {
360
            $column = $this->setupOrderColumn($orderable);
361
            if (isset($this->columnDef['order'][$column])) {
362
                $method     = $this->columnDef['order'][$column]['method'];
363
                $parameters = $this->columnDef['order'][$column]['parameters'];
364
                $this->compileColumnQuery(
365
                    $this->getQueryBuilder(), $method, $parameters, $column, $orderable['direction']
366
                );
367
            } else {
368
                /**
369
                 * If we perform a select("*"), the ORDER BY clause will look like this:
370
                 * ORDER BY * ASC
371
                 * which causes a query exception
372
                 * The temporary fix is modify `*` column to `id` column
373
                 */
374
                if ($column === '*') {
375
                    $column = 'id';
376
                }
377
                $this->getQueryBuilder()->orderBy($column, $orderable['direction']);
378
            }
379
        }
380
    }
381
382
    /**
383
     * Get order by column name.
384
     *
385
     * @param array $orderable
386
     * @return string
387
     */
388
    private function setupOrderColumn(array $orderable)
389
    {
390
        $r_column = $this->request->input('columns')[$orderable['column']];
391
        $column   = isset($r_column['name']) ? $r_column['name'] : $r_column['data'];
392
        if ($column >= 0) {
393
            $column = $this->setupColumnName($orderable['column'], true);
394
395
            return $column;
396
        }
397
398
        return $column;
399
    }
400
401
    /**
402
     * @inheritdoc
403
     */
404
    public function paging()
405
    {
406
        $this->query->skip($this->request['start'])
407
                    ->take((int) $this->request['length'] > 0 ? $this->request['length'] : 10);
408
    }
409
410
    /**
411
     * Get results
412
     *
413
     * @return array|static[]
414
     */
415
    public function results()
416
    {
417
        return $this->query->get();
418
    }
419
}
420