Passed
Push — master ( 98d5bf...9aa510 )
by Adam
03:07
created

DataTables::provideQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
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\IntegerType;
9
use Doctrine\DBAL\Types\SmallIntType;
10
use Illuminate\Database\DatabaseManager;
11
use Illuminate\Database\Eloquent\Builder;
12
use Illuminate\Database\Eloquent\Model;
13
use Illuminate\Http\JsonResponse;
14
use Illuminate\Http\Request;
15
use Illuminate\Http\Response;
16
use Illuminate\Support\Arr;
17
18
class DataTables
19
{
20
    /** @var Request $request */
21
    private $request;
22
23
    /** @var string $table */
24
    private $table;
25
26
    /** @var array $tableColumns */
27
    private $tableColumns;
28
29
    /** @var Model $model */
30
    private $model;
31
32
    /** @var Builder $query */
33
    private $query;
34
35
    /** @var Builder $query */
36
    private $originalQuery;
37
38
    /** @var array|null $aliases */
39
    private $aliases;
40
41
    /** @var DatabaseManager */
42
    private $DB;
43
44
    public function __construct(Request $request, DatabaseManager $DB)
45
    {
46
        $this->request = $request;
47
        $this->DB = $DB;
48
    }
49
50
    /**
51
     * @param \Illuminate\Database\Eloquent\Model   $model
52
     * @param \Illuminate\Database\Eloquent\Builder $query
53
     * @param array                                 $aliases
54
     *
55
     * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
56
     */
57
    public function provide(Model $model, Builder $query = null, array $aliases = null)
58
    {
59
        $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

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

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

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