Passed
Push — master ( 09fb0a...190064 )
by Adam
02:10
created

DataTables   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 305
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 150
dl 0
loc 305
rs 6.96
c 0
b 0
f 0
wmc 53

17 Methods

Rating   Name   Duplication   Size   Complexity  
A isDateRange() 0 4 2
A toMySQLDate() 0 5 1
A prepareSelects() 0 25 6
A getSearchQuery() 0 22 3
A provide() 0 41 5
A __construct() 0 4 1
A applySearch() 0 16 5
A getField() 0 10 4
A applyQueryOrder() 0 12 4
A applyOrder() 0 8 2
A shouldUseLike() 0 9 4
A getSearchMethod() 0 30 3
A getCount() 0 11 3
A applyPagination() 0 8 3
A withWheres() 0 4 2
A setResultCounters() 0 11 3
A withHavings() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like DataTables often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DataTables, and based on these observations, apply Extract Interface, too.

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

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