Passed
Push — master ( 190064...98d5bf )
by Adam
05:02
created

DataTables::removeKeyQuotes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 6
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 12
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
        if ($this->request->has(['draw', 'start', 'length'])) {
60
            $this->model = $model;
61
            $this->query = $query ?? $this->model->newQuery();
62
            $this->aliases = $aliases;
63
64
            $this->table = $model->getTable();
65
            $this->tableColumns = $this->DB
66
                ->connection($model->getConnectionName())
67
                ->getDoctrineSchemaManager()
68
                ->listTableColumns($this->table);
69
            $this->tableColumns = $this->removeKeyQuotes($this->tableColumns);
70
71
            $reqData = $this->request->all();
72
            $response = [];
73
74
            $this->prepareSelects();
75
            $this->originalQuery = clone $this->query;
76
77
            if (array_key_exists('columns', $reqData) && is_array($reqData['columns'])) {
78
                $columns = $reqData['columns'];
79
80
                if (is_array($reqData['columns'])) {
81
                    $this->applySearch($columns);
82
                    $this->applyOrder($reqData, $columns);
83
                }
84
            }
85
86
            $response['draw'] = +$reqData['draw'];
87
            $response = $this->setResultCounters($response);
88
89
            $this->applyPagination($reqData);
90
91
            $response['data'] = $this->query
92
                ->get()
93
                ->toArray();
94
95
            return new JsonResponse($response);
96
        }
97
98
        return new Response('', 400);
99
    }
100
101
    private function removeKeyQuotes($array)
102
    {
103
        foreach ($array as $key => $value) {
104
            $newKey = str_replace('`', '', $key);
105
106
            if ($key !== $newKey) {
107
                $array[$newKey] = $value;
108
                unset($array[$key]);
109
            }
110
        }
111
112
        return $array;
113
    }
114
115
    private function prepareSelects()
116
    {
117
        $tableAttr = array_keys(
118
            array_diff_key(
119
                array_flip(
120
                    array_keys($this->tableColumns)
121
                ),
122
                array_flip($this->model->getHidden())
123
            )
124
        );
125
126
        if (!empty($tableAttr)) {
127
            foreach ($tableAttr as $attr) {
128
                $selects[] = $this->DB->raw($this->table.'.'.$attr);
129
            }
130
        }
131
132
        if ($this->aliases) {
133
            foreach ($this->aliases as $alias => $value) {
134
                $selects[] = $this->DB->raw($value.' AS '.$alias);
135
            }
136
        }
137
138
        if (isset($selects)) {
139
            $this->query->select($selects);
140
        }
141
    }
142
143
    private function applySearch(array $columns)
144
    {
145
        foreach ($columns as $column) {
146
            $searchValue = Arr::get($column, 'search.value');
147
            $searchColumn = Arr::get($column, 'data');
148
149
            if (!is_null($searchValue) && !is_null($searchColumn)) {
150
                $searchField = $this->getField($searchColumn);
151
                if (!$searchField) {
152
                    continue;
153
                }
154
155
                $searchMethod = $this->getSearchMethod($searchField);
156
                [$searchQuery, $searchBindings] = $this->getSearchQuery($searchField, $searchValue, $searchColumn);
157
158
                $this->query->{$searchMethod}($searchQuery, $searchBindings);
159
            }
160
        }
161
    }
162
163
    private function getSearchQuery($searchField, $searchValue, $column)
164
    {
165
        if ($this->isDateRange($searchValue)) {
166
            [$from, $to] = explode(' - ', $searchValue);
167
168
            $from = $this->toMySQLDate($from);
169
            $to = $this->toMySQLDate($to, 1);
170
171
            return [
172
                $searchField.' between ? and ?',
173
                [$from, $to],
174
            ];
175
        } else {
176
            if ($this->shouldUseLike($this->tableColumns, $column)) {
177
                return [
178
                    $searchField.' like ?',
179
                    ['%'.$searchValue.'%'],
180
                ];
181
            } else {
182
                return [
183
                    $searchField.' = ?',
184
                    [$searchValue],
185
                ];
186
            }
187
        }
188
    }
189
190
    private function isDateRange($value) : bool
191
    {
192
        return (bool) (strlen($value) === 23) &&
193
            preg_match('^\\d{2}/\\d{2}/\\d{4} - \\d{2}/\\d{2}/\\d{4}^', $value);
194
    }
195
196
    private function toMySQLDate($value, $plusDay = 0)
197
    {
198
        return Carbon::createFromFormat('d/m/Y', $value)
199
            ->addDays($plusDay)
200
            ->toDateString();
201
    }
202
203
    private function applyOrder(array $reqData, array $columns)
204
    {
205
        if (array_key_exists('order', $reqData)) {
206
            $orderColumnId = Arr::get($reqData, 'order.0.column');
207
            $orderByColumn = Arr::get($columns, $orderColumnId.'.data');
208
            $direction = Arr::get($reqData, 'order.0.dir');
209
210
            $this->applyQueryOrder($orderByColumn, $direction);
211
        }
212
    }
213
214
    private function applyQueryOrder($orderByColumn, $direction)
215
    {
216
        if ($direction !== 'asc' && $direction !== 'desc') {
217
            return;
218
        }
219
220
        $orderField = $this->getField($orderByColumn);
221
        if (!$orderField) {
222
            return;
223
        }
224
225
        $this->query->orderByRaw($orderField.' '.$direction);
226
    }
227
228
    private function getField($column)
229
    {
230
        if (empty($this->aliases) || !array_key_exists($column, $this->aliases)) {
231
            if (array_key_exists($column, $this->tableColumns)) {
232
                return $this->table.'.'.$column;
233
            } else {
234
                return null;
235
            }
236
        } else {
237
            return $this->aliases[$column];
238
        }
239
    }
240
241
    private function setResultCounters(array $response) : array
242
    {
243
        $response['recordsTotal'] = $this->getCount($this->originalQuery);
244
245
        if ($this->withWheres() || $this->withHavings()) {
246
            $response['recordsFiltered'] = $this->getCount($this->query);
247
        } else {
248
            $response['recordsFiltered'] = $response['recordsTotal'];
249
        }
250
251
        return $response;
252
    }
253
254
    private function withWheres()
255
    {
256
        return !empty($this->query->getQuery()->wheres) &&
257
            $this->originalQuery->getQuery()->wheres !== $this->query->getQuery()->wheres;
258
    }
259
260
    private function withHavings()
261
    {
262
        return !empty($this->query->getQuery()->havings) &&
263
            $this->originalQuery->getQuery()->havings !== $this->query->getQuery()->havings;
264
    }
265
266
    private function getCount(Builder $query) : int
267
    {
268
        if (!empty($query->getQuery()->groups) || !empty($query->getQuery()->havings)) {
269
            return $this->DB
270
                ->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

270
                ->table($this->DB->raw('('./** @scrutinizer ignore-type */ $query->toSql().') as s'))
Loading history...
271
                ->setBindings($query->getQuery()->getBindings())
272
                ->selectRaw('count(*) as count')
273
                ->first()
274
                ->count;
275
        } else {
276
            return $query->getQuery()->count();
277
        }
278
    }
279
280
    private function applyPagination(array $reqData)
281
    {
282
        if (array_key_exists('start', $reqData)) {
283
            $this->query->offset(+$reqData['start']);
284
        }
285
286
        if (array_key_exists('length', $reqData)) {
287
            $this->query->limit(+$reqData['length']);
288
        }
289
    }
290
291
    private function getSearchMethod($alias)
292
    {
293
        $aggregate = [
294
            'AVG',
295
            'BIT_AND',
296
            'BIT_OR',
297
            'BIT_XOR',
298
            'COUNT',
299
            'GROUP_CONCAT',
300
            'JSON_ARRAYAGG',
301
            'JSON_OBJECTAGG',
302
            'MAX',
303
            'MIN',
304
            'STD',
305
            'STDDEV',
306
            'STDDEV_POP',
307
            'STDDEV_SAMP',
308
            'SUM',
309
            'VAR_POP',
310
            'VAR_SAMP',
311
            'VARIANCE',
312
        ];
313
314
        foreach ($aggregate as $m) {
315
            if (strpos($alias, $m) !== false) {
316
                return 'havingRaw';
317
            }
318
        }
319
320
        return 'whereRaw';
321
    }
322
323
    /**
324
     * @param Column[] $tableColumns
325
     * @param string   $column
326
     *
327
     * @return mixed
328
     */
329
    private function shouldUseLike($tableColumns, $column)
330
    {
331
        if (!array_key_exists($column, $tableColumns)) {
332
            return true;
333
        }
334
335
        return !($tableColumns[$column]->getType() instanceof IntegerType ||
336
            $tableColumns[$column]->getType() instanceof SmallIntType ||
337
            $tableColumns[$column]->getType() instanceof BigIntType);
338
    }
339
}
340