Completed
Branch 6.0 (46b375)
by Arjay
02:28
created

regexCaseInsensitiveColumnSearch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 8
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 8
loc 8
rs 9.4285
cc 2
eloc 5
nc 2
nop 2
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);
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 mixed $column
313
     * @param string $keyword
314
     */
315
    protected function caseInsensitiveColumnSearch($i, $column, $keyword)
316
    {
317
        if ($this->request->isRegex($i)) {
318
            $this->regexCaseInsensitiveColumnSearch($column, $keyword);
319
        } else {
320
            $this->query->whereRaw('LOWER(' . $column . ') LIKE ?', [Str::lower($keyword)]);
321
        }
322
    }
323
324
    /**
325
     * Perform regex case insensitive column search.
326
     *
327
     * @param mixed $column
328
     * @param string $keyword
329
     */
330 View Code Duplication
    protected function regexCaseInsensitiveColumnSearch($column, $keyword)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
331
    {
332
        if ($this->isOracleSql()) {
333
            $this->query->whereRaw('REGEXP_LIKE( LOWER(' . $column . ') , ?, \'i\' )', [$keyword]);
334
        } else {
335
            $this->query->whereRaw('LOWER(' . $column . ') REGEXP ?', [Str::lower($keyword)]);
336
        }
337
    }
338
339
    /**
340
     * Perform case sensitive column search.
341
     *
342
     * @param int $i
343
     * @param mixed $column
344
     * @param string $keyword
345
     */
346
    protected function caseSensitiveColumnSearch($i, $column, $keyword)
347
    {
348
        if ($this->request->isRegex($i)) {
349
            $this->regexCaseSensitiveColumnSearch($column, $keyword);
350
        } else {
351
            $this->query->whereRaw($column . ' LIKE ?', [$keyword]);
352
        }
353
    }
354
355
    /**
356
     * Perform regex case insensitive column search.
357
     * @param mixed $column
358
     * @param string $keyword
359
     */
360 View Code Duplication
    protected function regexCaseSensitiveColumnSearch($column, $keyword)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
361
    {
362
        if ($this->isOracleSql()) {
363
            $this->query->whereRaw('REGEXP_LIKE( ' . $column . ' , ? )', [$keyword]);
364
        } else {
365
            $this->query->whereRaw($column . ' REGEXP ?', [$keyword]);
366
        }
367
    }
368
369
    /**
370
     * @inheritdoc
371
     */
372
    public function ordering()
373
    {
374
        if ($this->orderCallback) {
375
            call_user_func($this->orderCallback, $this->getQueryBuilder());
376
377
            return;
378
        }
379
380
        foreach ($this->request->orderableColumns() as $orderable) {
381
            $column = $this->setupOrderColumn($orderable);
382
            if (isset($this->columnDef['order'][$column])) {
383
                $method     = $this->columnDef['order'][$column]['method'];
384
                $parameters = $this->columnDef['order'][$column]['parameters'];
385
                $this->compileColumnQuery(
386
                    $this->getQueryBuilder(), $method, $parameters, $column, $orderable['direction']
387
                );
388
            } else {
389
                /**
390
                 * If we perform a select("*"), the ORDER BY clause will look like this:
391
                 * ORDER BY * ASC
392
                 * which causes a query exception
393
                 * The temporary fix is modify `*` column to `id` column
394
                 */
395
                if ($column === '*') {
396
                    $column = 'id';
397
                }
398
                $this->getQueryBuilder()->orderBy($column, $orderable['direction']);
399
            }
400
        }
401
    }
402
403
    /**
404
     * Get order by column name.
405
     *
406
     * @param array $orderable
407
     * @return string
408
     */
409
    private function setupOrderColumn(array $orderable)
410
    {
411
        $r_column = $this->request->input('columns')[$orderable['column']];
412
        $column   = isset($r_column['name']) ? $r_column['name'] : $r_column['data'];
413
        if ($column >= 0) {
414
            $column = $this->setupColumnName($orderable['column'], true);
415
416
            return $column;
417
        }
418
419
        return $column;
420
    }
421
422
    /**
423
     * @inheritdoc
424
     */
425
    public function paging()
426
    {
427
        $this->query->skip($this->request['start'])
428
                    ->take((int) $this->request['length'] > 0 ? $this->request['length'] : 10);
429
    }
430
431
    /**
432
     * Get results
433
     *
434
     * @return array|static[]
435
     */
436
    public function results()
437
    {
438
        return $this->query->get();
439
    }
440
}
441