Passed
Push — master ( 11499a...b0e9fb )
by Adam
03:32
created

DataTables::withWheres()   A

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
    /** @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
    public function __construct(Request $request, DatabaseManager $DB)
46
    {
47
        $this->reqData = $request->all();
48
        $this->DB = $DB;
49
    }
50
51
    public function withInput(array $requestData)
52
    {
53
        $this->reqData = $requestData;
54
55
        return $this;
56
    }
57
58
    /**
59
     * @param \Illuminate\Database\Eloquent\Model   $model
60
     * @param \Illuminate\Database\Eloquent\Builder $query
61
     * @param array                                 $aliases
62
     *
63
     * @return \Illuminate\Http\JsonResponse
64
     */
65
    public function provide(Model $model, Builder $query = null, array $aliases = null): JsonResponse
66
    {
67
        $query = $this->provider(...func_get_args());
0 ignored issues
show
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...
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

67
        $query = $this->provider(/** @scrutinizer ignore-type */ ...func_get_args());
Loading history...
68
69
        if ($query) {
0 ignored issues
show
introduced by
$query is of type null, thus it always evaluated to false.
Loading history...
70
            $response = [];
71
72
            $response['draw'] = +$this->reqData['draw'];
73
            $response = $this->setResultCounters($response);
74
75
            $this->applyPagination();
76
77
            $response['data'] = $this->query
78
                ->get()
79
                ->toArray();
80
81
            return new JsonResponse($response);
82
        }
83
84
        return new JsonResponse('', 400);
85
    }
86
87
    /**
88
     * @param \Illuminate\Database\Eloquent\Model   $model
89
     * @param \Illuminate\Database\Eloquent\Builder $query
90
     * @param array                                 $aliases
91
     *
92
     * @return \Illuminate\Database\Eloquent\Builder
93
     */
94
    public function provideQuery(Model $model, Builder $query = null, array $aliases = null): ?Builder
95
    {
96
        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

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