Passed
Branch master (d2ea4c)
by Adam
03:13
created

DataTables   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 272
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 129
dl 0
loc 272
rs 8.4
c 0
b 0
f 0
wmc 50

14 Methods

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

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 $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 ($tableAttr) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tableAttr of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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
194
            if (!is_null($orderColumnId) && !is_null($orderByColumn)) {
195
                $direction = Arr::get($reqData, 'order.0.dir');
196
                if ($direction !== 'asc' && $direction !== 'desc') {
197
                    return;
198
                }
199
200
                $orderField = $this->getField($orderByColumn);
201
                if (!$orderField) {
202
                    return;
203
                }
204
205
                $this->query->orderByRaw($orderField.' '.$direction);
206
            }
207
        }
208
    }
209
210
    private function getField($column)
211
    {
212
        if (empty($this->aliases) || !array_key_exists($column, $this->aliases)) {
213
            if (array_key_exists($column, $this->tableColumns)) {
214
                return $this->table.'.'.$column;
215
            } else {
216
                return null;
217
            }
218
        } else {
219
            return $this->aliases[$column];
220
        }
221
    }
222
223
    private function setResultCounters(array $response) : array
224
    {
225
        $response['recordsTotal'] = $this->getCount($this->originalQuery);
226
227
        if (!empty($this->query->getQuery()->wheres) &&
228
            $this->originalQuery->getQuery()->wheres !== $this->query->getQuery()->wheres) {
229
            $response['recordsFiltered'] = $this->getCount($this->query);
230
        } else {
231
            $response['recordsFiltered'] = $response['recordsTotal'];
232
        }
233
234
        return $response;
235
    }
236
237
    private function getCount(Builder $query) : int
238
    {
239
        if (!empty($query->getQuery()->groups) || !empty($query->getQuery()->havings)) {
240
            return $this->DB
241
                ->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

241
                ->table($this->DB->raw('('./** @scrutinizer ignore-type */ $query->toSql().') as s'))
Loading history...
242
                ->setBindings($query->getBindings())
0 ignored issues
show
Bug introduced by
It seems like $query->getBindings() can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $bindings of Illuminate\Database\Query\Builder::setBindings() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

242
                ->setBindings(/** @scrutinizer ignore-type */ $query->getBindings())
Loading history...
243
                ->selectRaw('count(*) as count')
244
                ->first()
245
                ->count;
246
        } else {
247
            return $query->count();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->count() could return the type Illuminate\Database\Eloquent\Builder which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
248
        }
249
    }
250
251
    private function applyPagination(array $reqData)
252
    {
253
        if (array_key_exists('start', $reqData)) {
254
            $this->query->offset(+$reqData['start']);
255
        }
256
257
        if (array_key_exists('length', $reqData)) {
258
            $this->query->limit(+$reqData['length']);
259
        }
260
    }
261
262
    private function getSearchMethod($alias)
263
    {
264
        $mustUseHaving = ['GROUP_CONCAT', 'COUNT', 'MIN', 'IFNULL'];
265
266
        foreach ($mustUseHaving as $m) {
267
            if (strpos($alias, $m) !== false) {
268
                return 'havingRaw';
269
            }
270
        }
271
272
        return 'whereRaw';
273
    }
274
275
    /**
276
     * @param Column[] $tableColumns
277
     * @param string   $column
278
     *
279
     * @return mixed
280
     */
281
    private function shouldUseLike($tableColumns, $column)
282
    {
283
        if (!array_key_exists($column, $tableColumns)) {
284
            return true;
285
        }
286
287
        return !($tableColumns[$column]->getType() instanceof IntegerType ||
288
            $tableColumns[$column]->getType() instanceof SmallIntType ||
289
            $tableColumns[$column]->getType() instanceof BigIntType);
290
    }
291
}
292