Completed
Branch 6.0 (f63a90)
by Arjay
02:17
created

QueryBuilderEngine::caseInsensitiveColumnSearch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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