1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace hamburgscleanest\DataTables\Models; |
4
|
|
|
|
5
|
|
|
use hamburgscleanest\DataTables\Exceptions\MultipleComponentAssertionException; |
6
|
|
|
use hamburgscleanest\DataTables\Facades\TableRenderer; |
7
|
|
|
use hamburgscleanest\DataTables\Interfaces\ColumnFormatter; |
8
|
|
|
use hamburgscleanest\DataTables\Interfaces\HeaderFormatter; |
9
|
|
|
use hamburgscleanest\DataTables\Models\Cache\Cache; |
10
|
|
|
use hamburgscleanest\DataTables\Models\Cache\NoCache; |
11
|
|
|
use hamburgscleanest\DataTables\Models\Column\Column; |
12
|
|
|
use Illuminate\Database\Eloquent\Builder; |
13
|
|
|
use Illuminate\Database\Eloquent\Model; |
14
|
|
|
use Illuminate\Support\Collection; |
15
|
|
|
use RuntimeException; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Class DataTable |
19
|
|
|
* @package hamburgscleanest\DataTables\Models |
20
|
|
|
*/ |
21
|
|
|
class DataTable { |
22
|
|
|
|
23
|
|
|
/** @var Builder */ |
24
|
|
|
private $_queryBuilder; |
25
|
|
|
|
26
|
|
|
/** @var array */ |
27
|
|
|
private $_headerFormatters = []; |
28
|
|
|
|
29
|
|
|
/** @var array */ |
30
|
|
|
private $_components = []; |
31
|
|
|
|
32
|
|
|
/** @var string */ |
33
|
|
|
private $_classes; |
34
|
|
|
|
35
|
|
|
/** @var array */ |
36
|
|
|
private $_columns = []; |
37
|
|
|
|
38
|
|
|
/** @var Model */ |
39
|
|
|
private $_model; |
40
|
|
|
|
41
|
|
|
/** @var array */ |
42
|
|
|
private $_relations = []; |
43
|
|
|
|
44
|
|
|
/** @var string */ |
45
|
|
|
private $_noDataHtml = '<div>no data</div>'; |
46
|
|
|
|
47
|
|
|
/** @var Cache */ |
48
|
|
|
private $_cache; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Set the base model whose data is displayed in the table. |
52
|
|
|
* |
53
|
|
|
* @param string $modelName |
54
|
|
|
* @param array $columns |
55
|
|
|
* @param Cache|null $cache |
56
|
|
|
* @return $this|DataTable |
57
|
|
|
* @throws \RuntimeException |
58
|
|
|
*/ |
59
|
71 |
|
public function model(string $modelName, array $columns = [], Cache $cache = null) : DataTable |
60
|
|
|
{ |
61
|
71 |
|
if (!\is_subclass_of($modelName, Model::class)) |
62
|
|
|
{ |
63
|
2 |
|
throw new RuntimeException('Class "' . $modelName . '" is not an active record!'); |
64
|
|
|
} |
65
|
|
|
|
66
|
69 |
|
$this->_model = new $modelName; |
67
|
69 |
|
$this->_queryBuilder = $this->_model->newQuery(); |
68
|
69 |
|
$this->_columns = $this->_fetchColumns($columns); |
69
|
69 |
|
$this->_cache = $cache ?? new NoCache(); |
|
|
|
|
70
|
|
|
|
71
|
69 |
|
return $this; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Returns an array of Column objects which may be bound to a formatter. |
76
|
|
|
* |
77
|
|
|
* @param array $columns |
78
|
|
|
* @return array |
79
|
|
|
*/ |
80
|
70 |
|
private function _fetchColumns(array $columns) : array |
81
|
|
|
{ |
82
|
70 |
|
$columnModels = []; |
83
|
70 |
|
foreach ($columns as $column => $formatter) |
84
|
|
|
{ |
85
|
57 |
|
[$column, $formatter] = $this->_setColumnFormatter($column, $formatter); |
86
|
57 |
|
$columnModels[] = new Column($column, $formatter, $this->_model); |
87
|
|
|
} |
88
|
|
|
|
89
|
70 |
|
return $columnModels; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* @param $column |
94
|
|
|
* @param $formatter |
95
|
|
|
* @return array |
96
|
|
|
*/ |
97
|
57 |
|
private function _setColumnFormatter($column, $formatter) : array |
98
|
|
|
{ |
99
|
57 |
|
if (\is_int($column)) |
100
|
|
|
{ |
101
|
55 |
|
$column = $formatter; |
102
|
55 |
|
$formatter = null; |
103
|
|
|
} |
104
|
|
|
|
105
|
57 |
|
return [$column, $formatter]; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* @param Cache $cache |
110
|
|
|
* @return DataTable |
111
|
|
|
*/ |
112
|
|
|
public function cache(Cache $cache) : DataTable |
113
|
|
|
{ |
114
|
|
|
$this->_cache = $cache; |
115
|
|
|
|
116
|
|
|
return $this; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* @return array |
121
|
|
|
*/ |
122
|
5 |
|
public function getColumns() : array |
123
|
|
|
{ |
124
|
5 |
|
return $this->_columns; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* @return Builder |
129
|
|
|
*/ |
130
|
51 |
|
public function query() : Builder |
131
|
|
|
{ |
132
|
51 |
|
return $this->_queryBuilder; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Displayed columns |
137
|
|
|
* |
138
|
|
|
* @param array $columns |
139
|
|
|
* @return $this |
140
|
|
|
*/ |
141
|
3 |
|
public function columns(array $columns) : DataTable |
142
|
|
|
{ |
143
|
3 |
|
$this->_columns += $this->_fetchColumns($columns); |
144
|
|
|
|
145
|
3 |
|
return $this; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Add a component to the data table. |
150
|
|
|
* For example a "Paginator" or a "Sorter". |
151
|
|
|
* |
152
|
|
|
* @param DataComponent $component |
153
|
|
|
* |
154
|
|
|
* @param null|string $name |
155
|
|
|
* @return DataTable |
156
|
|
|
* @throws \hamburgscleanest\DataTables\Exceptions\MultipleComponentAssertionException |
157
|
|
|
*/ |
158
|
33 |
|
public function addComponent(DataComponent $component, ? string $name = null) : DataTable |
159
|
|
|
{ |
160
|
33 |
|
$componentName = $this->_getComponentName($component, $name); |
161
|
33 |
|
if ($this->componentExists($componentName)) |
162
|
|
|
{ |
163
|
1 |
|
throw new MultipleComponentAssertionException(); |
164
|
|
|
} |
165
|
|
|
|
166
|
33 |
|
$this->_components[$componentName] = $component->init($this); |
167
|
|
|
|
168
|
33 |
|
return $this; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* @param DataComponent $component |
173
|
|
|
* @param null|string $name |
174
|
|
|
* @return string |
175
|
|
|
*/ |
176
|
33 |
|
private function _getComponentName(DataComponent $component, ? string $name = null) : string |
177
|
|
|
{ |
178
|
33 |
|
if ($name !== null) |
179
|
|
|
{ |
180
|
2 |
|
return \str_replace(' ', '', \mb_strtolower($name)); |
181
|
|
|
} |
182
|
|
|
|
183
|
31 |
|
return $component->getName(); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* Check whether a component exists for the given data table. |
188
|
|
|
* @param string $componentName |
189
|
|
|
* @return bool |
190
|
|
|
*/ |
191
|
33 |
|
public function componentExists(string $componentName) : bool |
192
|
|
|
{ |
193
|
33 |
|
return \array_key_exists($componentName, $this->_components); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Add a formatter for the column headers. |
198
|
|
|
* |
199
|
|
|
* @param HeaderFormatter $headerFormatter |
200
|
|
|
* @return DataTable |
201
|
|
|
*/ |
202
|
12 |
|
public function formatHeaders(HeaderFormatter $headerFormatter) : DataTable |
203
|
|
|
{ |
204
|
12 |
|
$this->_headerFormatters[] = $headerFormatter; |
205
|
|
|
|
206
|
12 |
|
return $this; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* Add a formatter for a column. |
211
|
|
|
* |
212
|
|
|
* @param string $columnName |
213
|
|
|
* @param ColumnFormatter $columnFormatter |
214
|
|
|
* @return DataTable |
215
|
|
|
*/ |
216
|
10 |
|
public function formatColumn(string $columnName, ColumnFormatter $columnFormatter) : DataTable |
217
|
|
|
{ |
218
|
|
|
/** @var Column $column */ |
219
|
10 |
|
$column = \array_first( |
220
|
10 |
|
$this->_columns, |
221
|
|
|
function($column) use ($columnName) { |
222
|
|
|
/** @var Column $column */ |
223
|
10 |
|
return $column->getName() === $columnName; |
224
|
10 |
|
} |
225
|
|
|
); |
226
|
|
|
|
227
|
10 |
|
if ($column !== null) |
228
|
|
|
{ |
229
|
10 |
|
$column->setFormatter($columnFormatter); |
230
|
|
|
} |
231
|
|
|
|
232
|
10 |
|
return $this; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* Add classes to the table. |
237
|
|
|
* |
238
|
|
|
* @param string $classes |
239
|
|
|
* |
240
|
|
|
* @return $this |
241
|
|
|
*/ |
242
|
1 |
|
public function classes(string $classes) : DataTable |
243
|
|
|
{ |
244
|
1 |
|
$this->_classes = $classes; |
245
|
|
|
|
246
|
1 |
|
return $this; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Add a relation to the table. |
251
|
|
|
* |
252
|
|
|
* @param array $relations |
253
|
|
|
* @return DataTable |
254
|
|
|
*/ |
255
|
4 |
|
public function with(array $relations) : DataTable |
256
|
|
|
{ |
257
|
4 |
|
$this->_relations += Relationship::createFromArray($this->_model, $relations); |
258
|
|
|
|
259
|
4 |
|
return $this; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Set the HTML which should be displayed when the dataset is empty. |
264
|
|
|
* |
265
|
|
|
* @param string $html |
266
|
|
|
* @return DataTable |
267
|
|
|
*/ |
268
|
1 |
|
public function noDataHtml(string $html) : DataTable |
269
|
|
|
{ |
270
|
1 |
|
$this->_noDataHtml = $html; |
271
|
|
|
|
272
|
1 |
|
return $this; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Set a view which should be displayed when the dataset is empty. |
277
|
|
|
* |
278
|
|
|
* @param string $viewName |
279
|
|
|
* @return DataTable |
280
|
|
|
* @throws \Throwable |
281
|
|
|
*/ |
282
|
1 |
|
public function noDataView(string $viewName) : DataTable |
283
|
|
|
{ |
284
|
1 |
|
$this->_noDataHtml = \view($viewName)->render(); |
285
|
|
|
|
286
|
1 |
|
return $this; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* Renders the table. |
291
|
|
|
* |
292
|
|
|
* @return string |
293
|
|
|
* @throws \RuntimeException |
294
|
|
|
*/ |
295
|
52 |
|
public function render() : string |
296
|
|
|
{ |
297
|
52 |
|
if ($this->_queryBuilder === null) |
298
|
|
|
{ |
299
|
1 |
|
throw new RuntimeException('Unknown base model!'); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
$data = $this->_cache->retrieve(function() { return $this->_getData(); }); |
|
|
|
|
303
|
51 |
|
if ($data->count() === 0) |
304
|
|
|
{ |
305
|
5 |
|
return $this->_noDataHtml; |
306
|
|
|
} |
307
|
|
|
|
308
|
46 |
|
return TableRenderer::open($this->_classes) . |
309
|
46 |
|
TableRenderer::renderHeaders($this->_fetchHeaders(), $this->_headerFormatters) . |
310
|
46 |
|
TableRenderer::renderBody($data, $this->_columns) . |
311
|
46 |
|
TableRenderer::close(); |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* Get data which should be displayed in the table. |
316
|
|
|
* |
317
|
|
|
* @return Collection |
318
|
|
|
*/ |
319
|
51 |
|
private function _getData() : Collection |
320
|
|
|
{ |
321
|
51 |
|
$this->_addRelations(); |
322
|
|
|
|
323
|
|
|
/** @var DataComponent $component */ |
324
|
51 |
|
foreach ($this->_components as $component) |
325
|
|
|
{ |
326
|
18 |
|
$component->transformData(); |
327
|
|
|
} |
328
|
|
|
|
329
|
51 |
|
return $this->_setSelection()->_queryBuilder->get(); |
330
|
|
|
} |
331
|
|
|
|
332
|
51 |
|
private function _addRelations() : void |
333
|
|
|
{ |
334
|
51 |
|
if (\count($this->_relations) === 0) |
335
|
|
|
{ |
336
|
47 |
|
return; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** @var Relationship $relation */ |
340
|
4 |
|
foreach ($this->_relations as $relation) |
341
|
|
|
{ |
342
|
4 |
|
$relation->addJoin($this->_queryBuilder); |
343
|
|
|
} |
344
|
|
|
|
345
|
4 |
|
$this->_queryBuilder->getQuery()->groupBy($this->_model->getTable() . '.' . $this->_model->getKeyName()); |
346
|
4 |
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* @return DataTable |
350
|
|
|
*/ |
351
|
51 |
|
private function _setSelection() : DataTable |
352
|
|
|
{ |
353
|
51 |
|
$query = $this->_queryBuilder->getQuery(); |
354
|
|
|
|
355
|
51 |
|
$columns = $this->_getColumnsForSelect(); |
356
|
51 |
|
if (!empty($columns)) |
357
|
|
|
{ |
358
|
45 |
|
$query->selectRaw( |
359
|
45 |
|
\implode(',', |
360
|
|
|
\array_map(function($column) { |
361
|
|
|
/** @var Column $column */ |
362
|
45 |
|
return $column->getIdentifier(); |
363
|
45 |
|
}, $columns) |
364
|
|
|
) |
365
|
|
|
); |
366
|
|
|
} |
367
|
|
|
|
368
|
51 |
|
return $this; |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* @return array |
373
|
|
|
*/ |
374
|
51 |
|
private function _getColumnsForSelect() : array |
375
|
|
|
{ |
376
|
51 |
|
return \array_filter( |
377
|
51 |
|
$this->_columns, |
378
|
|
|
function($column) { |
379
|
|
|
/** @var Column $column */ |
380
|
46 |
|
return !$column->isMutated(); |
381
|
51 |
|
} |
382
|
|
|
); |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
/** |
386
|
|
|
* @return array |
387
|
|
|
*/ |
388
|
46 |
|
private function _fetchHeaders() : array |
389
|
|
|
{ |
390
|
46 |
|
return \array_map( |
391
|
46 |
|
function($column) { |
392
|
|
|
/** @var Column $column */ |
393
|
44 |
|
return new Header($column->getKey()); |
394
|
46 |
|
}, |
395
|
46 |
|
$this->_columns |
396
|
|
|
); |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* @param $name |
401
|
|
|
* @return mixed |
402
|
|
|
*/ |
403
|
2 |
|
public function __get($name) |
404
|
|
|
{ |
405
|
2 |
|
if (\array_key_exists($name, $this->_components)) |
406
|
|
|
{ |
407
|
2 |
|
return $this->_components[$name]; |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
return $this->{$name}; |
411
|
|
|
} |
412
|
|
|
} |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.