Passed
Push — master ( d2ea4c...192115 )
by Adam
01:53
created

DataTables::isDateRange()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 4
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
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
            $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 (!empty($this->query->getQuery()->wheres) &&
231
            $this->originalQuery->getQuery()->wheres !== $this->query->getQuery()->wheres) {
232
            $response['recordsFiltered'] = $this->getCount($this->query);
233
        } else {
234
            $response['recordsFiltered'] = $response['recordsTotal'];
235
        }
236
237
        return $response;
238
    }
239
240
    private function getCount(Builder $query) : int
241
    {
242
        if (!empty($query->getQuery()->groups) || !empty($query->getQuery()->havings)) {
243
            return $this->DB
244
                ->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

244
                ->table($this->DB->raw('('./** @scrutinizer ignore-type */ $query->toSql().') as s'))
Loading history...
245
                ->setBindings($query->getQuery()->getBindings())
246
                ->selectRaw('count(*) as count')
247
                ->first()
248
                ->count;
249
        } else {
250
            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...
251
        }
252
    }
253
254
    private function applyPagination(array $reqData)
255
    {
256
        if (array_key_exists('start', $reqData)) {
257
            $this->query->offset(+$reqData['start']);
258
        }
259
260
        if (array_key_exists('length', $reqData)) {
261
            $this->query->limit(+$reqData['length']);
262
        }
263
    }
264
265
    private function getSearchMethod($alias)
266
    {
267
        $mustUseHaving = ['GROUP_CONCAT', 'COUNT', 'MIN', 'IFNULL'];
268
269
        foreach ($mustUseHaving as $m) {
270
            if (strpos($alias, $m) !== false) {
271
                return 'havingRaw';
272
            }
273
        }
274
275
        return 'whereRaw';
276
    }
277
278
    /**
279
     * @param Column[] $tableColumns
280
     * @param string   $column
281
     *
282
     * @return mixed
283
     */
284
    private function shouldUseLike($tableColumns, $column)
285
    {
286
        if (!array_key_exists($column, $tableColumns)) {
287
            return true;
288
        }
289
290
        return !($tableColumns[$column]->getType() instanceof IntegerType ||
291
            $tableColumns[$column]->getType() instanceof SmallIntType ||
292
            $tableColumns[$column]->getType() instanceof BigIntType);
293
    }
294
}
295