DataTables::withWheres()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 2
eloc 2
c 1
b 1
f 0
nc 2
nop 0
dl 0
loc 4
rs 10
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
    const SIMPLE_PAGINATION_RECORDS = 100000;
22
23
    /** @var array */
24
    private $reqData;
25
26
    /** @var string */
27
    private $table;
28
29
    /** @var array */
30
    private $tableColumns;
31
32
    /** @var Model */
33
    private $model;
34
35
    /** @var Builder */
36
    private $query;
37
38
    /** @var Builder */
39
    private $originalQuery;
40
41
    /** @var array|null */
42
    private $aliases;
43
44
    /** @var DatabaseManager */
45
    private $DB;
46
47
    /** @var int */
48
    private $totalRecordsCount = null;
49
50
    /** @var bool */
51
    private $simplePagination = false;
52
53
    public function __construct(Request $request, DatabaseManager $DB)
54
    {
55
        $this->reqData = $request->all();
56
        $this->DB = $DB;
57
    }
58
59
    public function withInput(array $requestData)
60
    {
61
        $this->reqData = $requestData;
62
63
        return $this;
64
    }
65
66
    public function simplePagination()
67
    {
68
        $this->simplePagination = true;
69
70
        return $this;
71
    }
72
73
    /**
74
     * @param \Illuminate\Database\Eloquent\Model   $model
75
     * @param \Illuminate\Database\Eloquent\Builder $query
76
     * @param array                                 $aliases
77
     *
78
     * @return \Illuminate\Http\JsonResponse
79
     */
80
    public function provide(Model $model, Builder $query = null, array $aliases = null): JsonResponse
81
    {
82
        $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

82
        $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...
83
84
        if ($query) {
0 ignored issues
show
introduced by
$query is of type null, thus it always evaluated to false.
Loading history...
85
            $response = [];
86
87
            $response['draw'] = +$this->reqData['draw'];
88
            $response = $this->setResultCounters($response);
89
90
            $this->applyPagination();
91
92
            $response['data'] = $this->query
93
                ->get()
94
                ->toArray();
95
96
            return new JsonResponse($response);
97
        }
98
99
        return new JsonResponse('', 400);
100
    }
101
102
    /**
103
     * @param \Illuminate\Database\Eloquent\Model   $model
104
     * @param \Illuminate\Database\Eloquent\Builder $query
105
     * @param array                                 $aliases
106
     *
107
     * @return \Illuminate\Database\Eloquent\Builder
108
     */
109
    public function provideQuery(Model $model, Builder $query = null, array $aliases = null): ?Builder
110
    {
111
        return $this->provider(...func_get_args());
0 ignored issues
show
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...
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

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