Passed
Push — master ( b0e9fb...07ed8a )
by Adam
03:59
created

DataTables::getSearchMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 30
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 3
eloc 23
c 2
b 0
f 1
nc 3
nop 1
dl 0
loc 30
rs 9.552
1
<?php
2
3
namespace AdMos\DataTables;
4
5
use Carbon\Carbon;
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Types\BigIntType;
8
use Doctrine\DBAL\Types\BooleanType;
9
use Doctrine\DBAL\Types\IntegerType;
10
use Doctrine\DBAL\Types\SmallIntType;
11
use Illuminate\Database\DatabaseManager;
12
use Illuminate\Database\Eloquent\Builder;
13
use Illuminate\Database\Eloquent\Model;
14
use Illuminate\Database\Query\Expression;
15
use Illuminate\Http\JsonResponse;
16
use Illuminate\Http\Request;
17
use Illuminate\Support\Arr;
18
19
class DataTables
20
{
21
    /** @var array */
22
    private $reqData;
23
24
    /** @var string */
25
    private $table;
26
27
    /** @var array */
28
    private $tableColumns;
29
30
    /** @var Model */
31
    private $model;
32
33
    /** @var Builder */
34
    private $query;
35
36
    /** @var Builder */
37
    private $originalQuery;
38
39
    /** @var array|null */
40
    private $aliases;
41
42
    /** @var DatabaseManager */
43
    private $DB;
44
45
    /** @var int */
46
    private $totalRecordsCount = null;
47
48
    public function __construct(Request $request, DatabaseManager $DB)
49
    {
50
        $this->reqData = $request->all();
51
        $this->DB = $DB;
52
    }
53
54
    public function withInput(array $requestData)
55
    {
56
        $this->reqData = $requestData;
57
58
        return $this;
59
    }
60
61
    /**
62
     * @param \Illuminate\Database\Eloquent\Model   $model
63
     * @param \Illuminate\Database\Eloquent\Builder $query
64
     * @param array                                 $aliases
65
     *
66
     * @return \Illuminate\Http\JsonResponse
67
     */
68
    public function provide(Model $model, Builder $query = null, array $aliases = null): JsonResponse
69
    {
70
        $query = $this->provider(...func_get_args());
0 ignored issues
show
Bug introduced by
func_get_args() is expanded, but the parameter $model of AdMos\DataTables\DataTables::provider() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

70
        $query = $this->provider(/** @scrutinizer ignore-type */ ...func_get_args());
Loading history...
Bug introduced by
Are you sure the assignment to $query is correct as $this->provider(func_get_args()) targeting AdMos\DataTables\DataTables::provider() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
71
72
        if ($query) {
0 ignored issues
show
introduced by
$query is of type null, thus it always evaluated to false.
Loading history...
73
            $response = [];
74
75
            $response['draw'] = +$this->reqData['draw'];
76
            $response = $this->setResultCounters($response);
77
78
            $this->applyPagination();
79
80
            $response['data'] = $this->query
81
                ->get()
82
                ->toArray();
83
84
            return new JsonResponse($response);
85
        }
86
87
        return new JsonResponse('', 400);
88
    }
89
90
    /**
91
     * @param \Illuminate\Database\Eloquent\Model   $model
92
     * @param \Illuminate\Database\Eloquent\Builder $query
93
     * @param array                                 $aliases
94
     *
95
     * @return \Illuminate\Database\Eloquent\Builder
96
     */
97
    public function provideQuery(Model $model, Builder $query = null, array $aliases = null): ?Builder
98
    {
99
        return $this->provider(...func_get_args());
0 ignored issues
show
Bug introduced by
func_get_args() is expanded, but the parameter $model of AdMos\DataTables\DataTables::provider() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

99
        return $this->provider(/** @scrutinizer ignore-type */ ...func_get_args());
Loading history...
Bug introduced by
Are you sure the usage of $this->provider(func_get_args()) targeting AdMos\DataTables\DataTables::provider() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
100
    }
101
102
    private function provider(Model $model, Builder $query = null, array $aliases = null): ?Builder
103
    {
104
        if (
105
            array_key_exists('draw', $this->reqData) &&
106
            array_key_exists('start', $this->reqData) &&
107
            array_key_exists('length', $this->reqData)
108
        ) {
109
            $this->model = $model;
110
            $this->query = $query ?? $this->model->newQuery();
111
            $this->aliases = $aliases;
112
113
            $this->table = $model->getTable();
114
            $this->tableColumns = $this->DB
115
                ->connection($model->getConnectionName())
116
                ->getDoctrineSchemaManager()
117
                ->listTableColumns($this->table);
118
            $this->tableColumns = $this->removeKeyQuotes($this->tableColumns);
119
120
            $this->prepareSelects();
121
            $this->wrapWheres();
122
123
            $this->originalQuery = clone $this->query;
124
125
            if (array_key_exists('columns', $this->reqData) && is_array($this->reqData['columns'])) {
126
                $columns = $this->reqData['columns'];
127
128
                if (is_array($this->reqData['columns'])) {
129
                    $this->applySearch($columns);
130
                    $this->applyOrder($columns);
131
                }
132
            }
133
134
            return $this->query;
135
        }
136
137
        return null;
138
    }
139
140
    public function setTotalRecordsCount(int $count)
141
    {
142
        $this->totalRecordsCount = $count;
143
144
        return $this;
145
    }
146
147
    private function wrapWheres()
148
    {
149
        $query = $this->query->getQuery();
150
151
        $nq = $query->forNestedWhere();
152
        $nq->mergeWheres($query->wheres, $query->bindings);
153
154
        $query->wheres = [];
155
        $query->bindings['where'] = [];
156
157
        $query->addNestedWhereQuery($nq);
158
    }
159
160
    private function removeKeyQuotes($array)
161
    {
162
        foreach ($array as $key => $value) {
163
            $newKey = str_replace('`', '', $key);
164
165
            if ($key !== $newKey) {
166
                $array[$newKey] = $value;
167
                unset($array[$key]);
168
            }
169
        }
170
171
        return $array;
172
    }
173
174
    private function prepareSelects()
175
    {
176
        $tableAttr = array_keys(
177
            array_diff_key(
178
                array_flip(
179
                    array_keys($this->tableColumns)
180
                ),
181
                array_flip($this->model->getHidden())
182
            )
183
        );
184
185
        if (!empty($tableAttr)) {
186
            foreach ($tableAttr as $attr) {
187
                $selects[] = $this->DB->raw($this->table.'.'.$attr);
188
            }
189
        }
190
191
        if ($this->aliases) {
192
            foreach ($this->aliases as $alias => $value) {
193
                $selects[] = $this->DB->raw($value.' AS '.$alias);
194
            }
195
        }
196
197
        if (isset($selects)) {
198
            $this->query->select($selects);
199
        }
200
    }
201
202
    private function applySearch(array $columns)
203
    {
204
        foreach ($columns as $column) {
205
            $searchValue = Arr::get($column, 'search.value');
206
            $searchColumn = Arr::get($column, 'data');
207
208
            if (!is_null($searchValue) && !is_null($searchColumn)) {
209
                $searchField = $this->getField($searchColumn);
210
                if (!$searchField) {
211
                    continue;
212
                }
213
214
                $searchMethod = $this->getSearchMethod($searchField);
215
                [$searchQuery, $searchBindings] = $this->getSearchQuery($searchField, $searchValue, $searchColumn);
216
217
                $this->query->{$searchMethod}($searchQuery, $searchBindings);
218
            }
219
        }
220
    }
221
222
    private function getSearchQuery($searchField, $searchValue, $column)
223
    {
224
        if ($this->isDateRange($searchValue)) {
225
            [$from, $to] = explode(' - ', $searchValue);
226
227
            $from = $this->toMySQLDate($from);
228
            $to = $this->toMySQLDate($to, 1);
229
230
            return [
231
                $searchField.' between ? and ?',
232
                [$from, $to],
233
            ];
234
        } else {
235
            if ($this->shouldUseLike($this->tableColumns, $column)) {
236
                return [
237
                    $searchField.' like ?',
238
                    ['%'.$searchValue.'%'],
239
                ];
240
            } else {
241
                return [
242
                    $searchField.' = ?',
243
                    [$searchValue],
244
                ];
245
            }
246
        }
247
    }
248
249
    private function isDateRange($value): bool
250
    {
251
        return (bool) (strlen($value) === 23) &&
252
            preg_match('^\\d{2}/\\d{2}/\\d{4} - \\d{2}/\\d{2}/\\d{4}^', $value);
253
    }
254
255
    private function toMySQLDate($value, $plusDay = 0)
256
    {
257
        return Carbon::createFromFormat('d/m/Y', $value)
258
            ->addDays($plusDay)
259
            ->toDateString();
260
    }
261
262
    private function applyOrder(array $columns)
263
    {
264
        if (array_key_exists('order', $this->reqData)) {
265
            $orderColumnId = Arr::get($this->reqData, 'order.0.column');
266
            $orderByColumn = Arr::get($columns, $orderColumnId.'.data');
267
            $direction = Arr::get($this->reqData, 'order.0.dir');
268
269
            $this->applyQueryOrder($orderByColumn, $direction);
270
        }
271
    }
272
273
    private function applyQueryOrder($orderByColumn, $direction)
274
    {
275
        if ($direction !== 'asc' && $direction !== 'desc') {
276
            return;
277
        }
278
279
        $orderField = $this->getField($orderByColumn);
280
        if (!$orderField) {
281
            return;
282
        }
283
284
        $this->query->orderByRaw($orderField.' '.$direction);
285
    }
286
287
    private function getField($column)
288
    {
289
        if (empty($this->aliases) || !array_key_exists($column, $this->aliases)) {
290
            if (array_key_exists($column, $this->tableColumns)) {
291
                return $this->table.'.'.$column;
292
            } else {
293
                return null;
294
            }
295
        } else {
296
            return $this->aliases[$column];
297
        }
298
    }
299
300
    private function setResultCounters(array $response): array
301
    {
302
        $response['recordsTotal'] = $this->totalRecordsCount ?? $this->getCount($this->originalQuery);
303
304
        if ($this->withWheres() || $this->withHavings()) {
305
            $response['recordsFiltered'] = $this->getCount($this->query);
306
        } else {
307
            $response['recordsFiltered'] = $response['recordsTotal'];
308
        }
309
310
        return $response;
311
    }
312
313
    private function withWheres()
314
    {
315
        return !empty($this->query->getQuery()->wheres) &&
316
            $this->originalQuery->getQuery()->wheres !== $this->query->getQuery()->wheres;
317
    }
318
319
    private function withHavings()
320
    {
321
        return !empty($this->query->getQuery()->havings) &&
322
            $this->originalQuery->getQuery()->havings !== $this->query->getQuery()->havings;
323
    }
324
325
    private function getCount(Builder $query): int
326
    {
327
        $countQuery = (clone $query)->getQuery();
328
        $countQuery->columns = [new Expression('0')];
329
330
        if (!empty($countQuery->groups) || !empty($countQuery->havings)) {
331
            return $this->DB
332
                ->table($this->DB->raw('('.$countQuery->toSql().') as s'))
333
                ->setBindings($countQuery->getBindings())
334
                ->selectRaw('count(*) as count')
335
                ->first()
336
                ->count;
337
        } else {
338
            return $countQuery->count();
339
        }
340
    }
341
342
    private function applyPagination()
343
    {
344
        if (array_key_exists('start', $this->reqData)) {
345
            $this->query->offset(+$this->reqData['start']);
346
        }
347
348
        if (array_key_exists('length', $this->reqData)) {
349
            $this->query->limit(+$this->reqData['length']);
350
        }
351
    }
352
353
    private function getSearchMethod($alias)
354
    {
355
        $aggregate = [
356
            'AVG',
357
            'BIT_AND',
358
            'BIT_OR',
359
            'BIT_XOR',
360
            'COUNT',
361
            'GROUP_CONCAT',
362
            'JSON_ARRAYAGG',
363
            'JSON_OBJECTAGG',
364
            'MAX',
365
            'MIN',
366
            'STD',
367
            'STDDEV',
368
            'STDDEV_POP',
369
            'STDDEV_SAMP',
370
            'SUM',
371
            'VAR_POP',
372
            'VAR_SAMP',
373
            'VARIANCE',
374
        ];
375
376
        foreach ($aggregate as $m) {
377
            if (strpos($alias, $m) !== false) {
378
                return 'havingRaw';
379
            }
380
        }
381
382
        return 'whereRaw';
383
    }
384
385
    /**
386
     * @param Column[] $tableColumns
387
     * @param string   $column
388
     *
389
     * @return mixed
390
     */
391
    private function shouldUseLike($tableColumns, $column)
392
    {
393
        if (!array_key_exists($column, $tableColumns)) {
394
            return true;
395
        }
396
397
        return !($tableColumns[$column]->getType() instanceof IntegerType ||
398
            $tableColumns[$column]->getType() instanceof SmallIntType ||
399
            $tableColumns[$column]->getType() instanceof BigIntType ||
400
            $tableColumns[$column]->getType() instanceof BooleanType);
401
    }
402
}
403