Passed
Push — master ( 333e8b...583f08 )
by Adam
04:16
created

DataTables::provideQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 2
b 0
f 0
nc 1
nop 3
dl 0
loc 3
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\Http\JsonResponse;
15
use Illuminate\Http\Request;
16
use Illuminate\Support\Arr;
17
18
class DataTables
19
{
20
    /** @var array */
21
    private $reqData;
22
23
    /** @var string */
24
    private $table;
25
26
    /** @var array */
27
    private $tableColumns;
28
29
    /** @var Model */
30
    private $model;
31
32
    /** @var Builder */
33
    private $query;
34
35
    /** @var Builder */
36
    private $originalQuery;
37
38
    /** @var array|null */
39
    private $aliases;
40
41
    /** @var DatabaseManager */
42
    private $DB;
43
44
    public function __construct(Request $request, DatabaseManager $DB)
45
    {
46
        $this->reqData = $request->all();
47
        $this->DB = $DB;
48
    }
49
50
    public function withInput(array $requestData)
51
    {
52
        $this->reqData = $requestData;
53
54
        return $this;
55
    }
56
57
    /**
58
     * @param \Illuminate\Database\Eloquent\Model   $model
59
     * @param \Illuminate\Database\Eloquent\Builder $query
60
     * @param array                                 $aliases
61
     *
62
     * @return \Illuminate\Http\JsonResponse
63
     */
64
    public function provide(Model $model, Builder $query = null, array $aliases = null): JsonResponse
65
    {
66
        $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

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

95
        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...
96
    }
97
98
    private function provider(Model $model, Builder $query = null, array $aliases = null): ?Builder
99
    {
100
        if (
101
            array_key_exists('draw', $this->reqData) &&
102
            array_key_exists('start', $this->reqData) &&
103
            array_key_exists('length', $this->reqData)
104
        ) {
105
            $this->model = $model;
106
            $this->query = $query ?? $this->model->newQuery();
107
            $this->aliases = $aliases;
108
109
            $this->table = $model->getTable();
110
            $this->tableColumns = $this->DB
111
                ->connection($model->getConnectionName())
112
                ->getDoctrineSchemaManager()
113
                ->listTableColumns($this->table);
114
            $this->tableColumns = $this->removeKeyQuotes($this->tableColumns);
115
116
            $this->prepareSelects();
117
            $this->originalQuery = clone $this->query;
118
119
            if (array_key_exists('columns', $this->reqData) && is_array($this->reqData['columns'])) {
120
                $columns = $this->reqData['columns'];
121
122
                if (is_array($this->reqData['columns'])) {
123
                    $this->applySearch($columns);
124
                    $this->applyOrder($this->reqData, $columns);
125
                }
126
            }
127
128
            return $this->query;
129
        }
130
131
        return null;
132
    }
133
134
    private function removeKeyQuotes($array)
135
    {
136
        foreach ($array as $key => $value) {
137
            $newKey = str_replace('`', '', $key);
138
139
            if ($key !== $newKey) {
140
                $array[$newKey] = $value;
141
                unset($array[$key]);
142
            }
143
        }
144
145
        return $array;
146
    }
147
148
    private function prepareSelects()
149
    {
150
        $tableAttr = array_keys(
151
            array_diff_key(
152
                array_flip(
153
                    array_keys($this->tableColumns)
154
                ),
155
                array_flip($this->model->getHidden())
156
            )
157
        );
158
159
        if (!empty($tableAttr)) {
160
            foreach ($tableAttr as $attr) {
161
                $selects[] = $this->DB->raw($this->table.'.'.$attr);
162
            }
163
        }
164
165
        if ($this->aliases) {
166
            foreach ($this->aliases as $alias => $value) {
167
                $selects[] = $this->DB->raw($value.' AS '.$alias);
168
            }
169
        }
170
171
        if (isset($selects)) {
172
            $this->query->select($selects);
173
        }
174
    }
175
176
    private function applySearch(array $columns)
177
    {
178
        foreach ($columns as $column) {
179
            $searchValue = Arr::get($column, 'search.value');
180
            $searchColumn = Arr::get($column, 'data');
181
182
            if (!is_null($searchValue) && !is_null($searchColumn)) {
183
                $searchField = $this->getField($searchColumn);
184
                if (!$searchField) {
185
                    continue;
186
                }
187
188
                $searchMethod = $this->getSearchMethod($searchField);
189
                [$searchQuery, $searchBindings] = $this->getSearchQuery($searchField, $searchValue, $searchColumn);
190
191
                $this->query->{$searchMethod}($searchQuery, $searchBindings);
192
            }
193
        }
194
    }
195
196
    private function getSearchQuery($searchField, $searchValue, $column)
197
    {
198
        if ($this->isDateRange($searchValue)) {
199
            [$from, $to] = explode(' - ', $searchValue);
200
201
            $from = $this->toMySQLDate($from);
202
            $to = $this->toMySQLDate($to, 1);
203
204
            return [
205
                $searchField.' between ? and ?',
206
                [$from, $to],
207
            ];
208
        } else {
209
            if ($this->shouldUseLike($this->tableColumns, $column)) {
210
                return [
211
                    $searchField.' like ?',
212
                    ['%'.$searchValue.'%'],
213
                ];
214
            } else {
215
                return [
216
                    $searchField.' = ?',
217
                    [$searchValue],
218
                ];
219
            }
220
        }
221
    }
222
223
    private function isDateRange($value): bool
224
    {
225
        return (bool) (strlen($value) === 23) &&
226
            preg_match('^\\d{2}/\\d{2}/\\d{4} - \\d{2}/\\d{2}/\\d{4}^', $value);
227
    }
228
229
    private function toMySQLDate($value, $plusDay = 0)
230
    {
231
        return Carbon::createFromFormat('d/m/Y', $value)
232
            ->addDays($plusDay)
233
            ->toDateString();
234
    }
235
236
    private function applyOrder(array $columns)
237
    {
238
        if (array_key_exists('order', $this->reqData)) {
239
            $orderColumnId = Arr::get($this->reqData, 'order.0.column');
240
            $orderByColumn = Arr::get($columns, $orderColumnId.'.data');
241
            $direction = Arr::get($this->reqData, 'order.0.dir');
242
243
            $this->applyQueryOrder($orderByColumn, $direction);
244
        }
245
    }
246
247
    private function applyQueryOrder($orderByColumn, $direction)
248
    {
249
        if ($direction !== 'asc' && $direction !== 'desc') {
250
            return;
251
        }
252
253
        $orderField = $this->getField($orderByColumn);
254
        if (!$orderField) {
255
            return;
256
        }
257
258
        $this->query->orderByRaw($orderField.' '.$direction);
259
    }
260
261
    private function getField($column)
262
    {
263
        if (empty($this->aliases) || !array_key_exists($column, $this->aliases)) {
264
            if (array_key_exists($column, $this->tableColumns)) {
265
                return $this->table.'.'.$column;
266
            } else {
267
                return null;
268
            }
269
        } else {
270
            return $this->aliases[$column];
271
        }
272
    }
273
274
    private function setResultCounters(array $response): array
275
    {
276
        $response['recordsTotal'] = $this->getCount($this->originalQuery);
277
278
        if ($this->withWheres() || $this->withHavings()) {
279
            $response['recordsFiltered'] = $this->getCount($this->query);
280
        } else {
281
            $response['recordsFiltered'] = $response['recordsTotal'];
282
        }
283
284
        return $response;
285
    }
286
287
    private function withWheres()
288
    {
289
        return !empty($this->query->getQuery()->wheres) &&
290
            $this->originalQuery->getQuery()->wheres !== $this->query->getQuery()->wheres;
291
    }
292
293
    private function withHavings()
294
    {
295
        return !empty($this->query->getQuery()->havings) &&
296
            $this->originalQuery->getQuery()->havings !== $this->query->getQuery()->havings;
297
    }
298
299
    private function getCount(Builder $query): int
300
    {
301
        if (!empty($query->getQuery()->groups) || !empty($query->getQuery()->havings)) {
302
            return $this->DB
303
                ->table($this->DB->raw('('.$query->toSql().') as s'))
0 ignored issues
show
Bug introduced by
Are you sure $query->toSql() of type Illuminate\Database\Eloquent\Builder|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

303
                ->table($this->DB->raw('('./** @scrutinizer ignore-type */ $query->toSql().') as s'))
Loading history...
304
                ->setBindings($query->getQuery()->getBindings())
305
                ->selectRaw('count(*) as count')
306
                ->first()
307
                ->count;
308
        } else {
309
            return $query->getQuery()->count();
310
        }
311
    }
312
313
    private function applyPagination()
314
    {
315
        if (array_key_exists('start', $this->reqData)) {
316
            $this->query->offset(+$this->reqData['start']);
317
        }
318
319
        if (array_key_exists('length', $this->reqData)) {
320
            $this->query->limit(+$this->reqData['length']);
321
        }
322
    }
323
324
    private function getSearchMethod($alias)
325
    {
326
        $aggregate = [
327
            'AVG',
328
            'BIT_AND',
329
            'BIT_OR',
330
            'BIT_XOR',
331
            'COUNT',
332
            'GROUP_CONCAT',
333
            'JSON_ARRAYAGG',
334
            'JSON_OBJECTAGG',
335
            'MAX',
336
            'MIN',
337
            'STD',
338
            'STDDEV',
339
            'STDDEV_POP',
340
            'STDDEV_SAMP',
341
            'SUM',
342
            'VAR_POP',
343
            'VAR_SAMP',
344
            'VARIANCE',
345
        ];
346
347
        foreach ($aggregate as $m) {
348
            if (strpos($alias, $m) !== false) {
349
                return 'havingRaw';
350
            }
351
        }
352
353
        return 'whereRaw';
354
    }
355
356
    /**
357
     * @param Column[] $tableColumns
358
     * @param string   $column
359
     *
360
     * @return mixed
361
     */
362
    private function shouldUseLike($tableColumns, $column)
363
    {
364
        if (!array_key_exists($column, $tableColumns)) {
365
            return true;
366
        }
367
368
        return !($tableColumns[$column]->getType() instanceof IntegerType ||
369
            $tableColumns[$column]->getType() instanceof SmallIntType ||
370
            $tableColumns[$column]->getType() instanceof BigIntType ||
371
            $tableColumns[$column]->getType() instanceof BooleanType);
372
    }
373
}
374