1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Yiisoft\Yii\DataView; |
6
|
|
|
|
7
|
|
|
use Closure; |
8
|
|
|
use InvalidArgumentException; |
9
|
|
|
use JsonException; |
10
|
|
|
use Yiisoft\Html\Html; |
11
|
|
|
use Yiisoft\Widget\Widget; |
12
|
|
|
use Yiisoft\Yii\DataView\Field\DataField; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* DetailView displays the detail of a single data. |
16
|
|
|
* |
17
|
|
|
* DetailView is best used for displaying a data in a regular format (e.g. each field is displayed using flexbox). |
18
|
|
|
* |
19
|
|
|
* The data can be either object or an associative array. |
20
|
|
|
* |
21
|
|
|
* DetailView uses the {@see data} property to determines which model should be displayed how they should be formatted. |
22
|
|
|
* |
23
|
|
|
* A typical usage of DetailView is as follows: |
24
|
|
|
* |
25
|
|
|
* ```php |
26
|
|
|
* <?= DetailView::widget() |
27
|
|
|
* ->data(['id' => 1, 'username' => 'tests 1', 'status' => true]) |
28
|
|
|
* ->fields( |
29
|
|
|
* DataField::create()->attribute('id'), |
30
|
|
|
* DataField::create()->attribute('username'), |
31
|
|
|
* DataField::create()->attribute('status'), |
32
|
|
|
* ) |
33
|
|
|
* ->render() |
34
|
|
|
* ``` |
35
|
|
|
*/ |
36
|
|
|
final class DetailView extends Widget |
37
|
|
|
{ |
38
|
|
|
private array $attributes = []; |
39
|
|
|
private array $containerAttributes = []; |
40
|
|
|
private array|object $data = []; |
41
|
|
|
private array $dataAttributes = []; |
42
|
|
|
private array $fields = []; |
43
|
|
|
private string $header = ''; |
44
|
|
|
private string $itemTemplate = "<div{dataAttributes}>\n{label}\n{value}\n</div>"; |
45
|
|
|
private array|Closure $labelAttributes = []; |
46
|
|
|
private string $labelTag = 'span'; |
47
|
|
|
private string $labelTemplate = '<{labelTag}{labelAttributes}>{label}</{labelTag}>'; |
48
|
|
|
private string $template = "<div{attributes}>\n<div{containerAttributes}>\n{header}\n{items}\n</div>\n</div>"; |
49
|
|
|
private array|Closure $valueAttributes = []; |
50
|
|
|
private string $valueFalse = 'false'; |
51
|
|
|
private string $valueTag = 'div'; |
52
|
|
|
private string $valueTemplate = '<{valueTag}{valueAttributes}>{value}</{valueTag}>'; |
53
|
|
|
private string $valueTrue = 'true'; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Returns a new instance with the HTML attributes. The following special options are recognized. |
57
|
|
|
* |
58
|
|
|
* @param array $values Attribute values indexed by attribute names. |
59
|
|
|
*/ |
60
|
4 |
|
public function attributes(array $values): self |
61
|
|
|
{ |
62
|
4 |
|
$new = clone $this; |
63
|
4 |
|
$new->attributes = $values; |
64
|
|
|
|
65
|
4 |
|
return $new; |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Returns a new instance with the HTML attributes for the container items. |
70
|
|
|
* |
71
|
|
|
* @param array $values Attribute values indexed by attribute names. |
72
|
|
|
*/ |
73
|
3 |
|
public function containerAttributes(array $values): self |
74
|
|
|
{ |
75
|
3 |
|
$new = clone $this; |
76
|
3 |
|
$new->containerAttributes = $values; |
77
|
|
|
|
78
|
3 |
|
return $new; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Return new instance with the data. |
83
|
|
|
* |
84
|
|
|
* @param array|object $data the data model whose details are to be displayed. This can be an instance, an |
85
|
|
|
* associative array, an object. |
86
|
|
|
*/ |
87
|
23 |
|
public function data(array|object $data): self |
88
|
|
|
{ |
89
|
23 |
|
$new = clone $this; |
90
|
23 |
|
$new->data = $data; |
91
|
|
|
|
92
|
23 |
|
return $new; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Returns a new instance with the HTML attributes for the container item. |
97
|
|
|
* |
98
|
|
|
* @param array $values Attribute values indexed by attribute names. |
99
|
|
|
*/ |
100
|
4 |
|
public function dataAttributes(array $values): self |
101
|
|
|
{ |
102
|
4 |
|
$new = clone $this; |
103
|
4 |
|
$new->dataAttributes = $values; |
104
|
|
|
|
105
|
4 |
|
return $new; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Return a new instance the specified fields. |
110
|
|
|
* |
111
|
|
|
* @param DataField ...$value The `DetailView` column configuration. Each object represents the configuration for |
112
|
|
|
* one particular DetailView column. For example, |
113
|
|
|
* |
114
|
|
|
* ```php |
115
|
|
|
* [ |
116
|
|
|
* DataField::create()->label('Name')->value($data->name), |
117
|
|
|
* ] |
118
|
|
|
* ``` |
119
|
|
|
*/ |
120
|
23 |
|
public function fields(DataField ...$value): self |
121
|
|
|
{ |
122
|
23 |
|
$new = clone $this; |
123
|
23 |
|
$new->fields = $value; |
124
|
|
|
|
125
|
23 |
|
return $new; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Return new instance with the header. |
130
|
|
|
* |
131
|
|
|
* @param string $value The header. |
132
|
|
|
*/ |
133
|
4 |
|
public function header(string $value): self |
134
|
|
|
{ |
135
|
4 |
|
$new = clone $this; |
136
|
4 |
|
$new->header = $value; |
137
|
|
|
|
138
|
4 |
|
return $new; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Return new instance with the item template. |
143
|
|
|
* |
144
|
|
|
* @param string $value The item template. |
145
|
|
|
*/ |
146
|
2 |
|
public function itemTemplate(string $value): self |
147
|
|
|
{ |
148
|
2 |
|
$new = clone $this; |
149
|
2 |
|
$new->itemTemplate = $value; |
150
|
|
|
|
151
|
2 |
|
return $new; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Returns a new instance with the HTML attributes for the label. |
156
|
|
|
* |
157
|
|
|
* @param array $values Attribute values indexed by attribute names. |
158
|
|
|
*/ |
159
|
4 |
|
public function labelAttributes(array $values): self |
160
|
|
|
{ |
161
|
4 |
|
$new = clone $this; |
162
|
4 |
|
$new->labelAttributes = $values; |
163
|
|
|
|
164
|
4 |
|
return $new; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Return new instance with the label tag. |
169
|
|
|
* |
170
|
|
|
* @param string $value The tag to use for the label. |
171
|
|
|
*/ |
172
|
2 |
|
public function labelTag(string $value): self |
173
|
|
|
{ |
174
|
2 |
|
$new = clone $this; |
175
|
2 |
|
$new->labelTag = $value; |
176
|
|
|
|
177
|
2 |
|
return $new; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* Return new instance with the label template. |
182
|
|
|
* |
183
|
|
|
* @param string $value The label template. |
184
|
|
|
*/ |
185
|
2 |
|
public function labelTemplate(string $value): self |
186
|
|
|
{ |
187
|
2 |
|
$new = clone $this; |
188
|
2 |
|
$new->labelTemplate = $value; |
189
|
|
|
|
190
|
2 |
|
return $new; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Return new instance with the template. |
195
|
|
|
* |
196
|
|
|
* @param string $value The template. |
197
|
|
|
*/ |
198
|
2 |
|
public function template(string $value): self |
199
|
|
|
{ |
200
|
2 |
|
$new = clone $this; |
201
|
2 |
|
$new->template = $value; |
202
|
|
|
|
203
|
2 |
|
return $new; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* Returns a new instance with the HTML attributes for the value. |
208
|
|
|
* |
209
|
|
|
* @param array $values Attribute values indexed by attribute names. |
210
|
|
|
*/ |
211
|
3 |
|
public function valueAttributes(array $values): self |
212
|
|
|
{ |
213
|
3 |
|
$new = clone $this; |
214
|
3 |
|
$new->valueAttributes = $values; |
215
|
|
|
|
216
|
3 |
|
return $new; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* Return new instance when the value is false. |
221
|
|
|
* |
222
|
|
|
* @param string $value The value when is false. |
223
|
|
|
*/ |
224
|
3 |
|
public function valueFalse(string $value): self |
225
|
|
|
{ |
226
|
3 |
|
$new = clone $this; |
227
|
3 |
|
$new->valueFalse = $value; |
228
|
|
|
|
229
|
3 |
|
return $new; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Return new instance with the value tag. |
234
|
|
|
* |
235
|
|
|
* @param string $value The tag to use for the value. |
236
|
|
|
*/ |
237
|
1 |
|
public function valueTag(string $value): self |
238
|
|
|
{ |
239
|
1 |
|
$new = clone $this; |
240
|
1 |
|
$new->valueTag = $value; |
241
|
|
|
|
242
|
1 |
|
return $new; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Return new instance with the value template. |
247
|
|
|
* |
248
|
|
|
* @param string $value The value template. |
249
|
|
|
*/ |
250
|
2 |
|
public function valueTemplate(string $value): self |
251
|
|
|
{ |
252
|
2 |
|
$new = clone $this; |
253
|
2 |
|
$new->valueTemplate = $value; |
254
|
|
|
|
255
|
2 |
|
return $new; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Return new instance when the value is true. |
260
|
|
|
* |
261
|
|
|
* @param string $value The value when is true. |
262
|
|
|
*/ |
263
|
4 |
|
public function valueTrue(string $value): self |
264
|
|
|
{ |
265
|
4 |
|
$new = clone $this; |
266
|
4 |
|
$new->valueTrue = $value; |
267
|
|
|
|
268
|
4 |
|
return $new; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* @throws JsonException |
273
|
|
|
*/ |
274
|
23 |
|
public function render(): string |
275
|
|
|
{ |
276
|
23 |
|
if ($this->renderItems() === '') { |
277
|
1 |
|
return ''; |
278
|
|
|
} |
279
|
|
|
|
280
|
20 |
|
return $this->removeDoubleLinesBreaks( |
281
|
20 |
|
strtr( |
282
|
20 |
|
$this->template, |
283
|
20 |
|
[ |
284
|
20 |
|
'{attributes}' => Html::renderTagAttributes($this->attributes), |
285
|
20 |
|
'{containerAttributes}' => Html::renderTagAttributes($this->containerAttributes), |
286
|
20 |
|
'{dataAttributes}' => Html::renderTagAttributes($this->dataAttributes), |
287
|
20 |
|
'{header}' => $this->header, |
288
|
20 |
|
'{items}' => $this->renderItems(), |
289
|
20 |
|
] |
290
|
20 |
|
) |
291
|
20 |
|
); |
292
|
|
|
} |
293
|
|
|
|
294
|
20 |
|
private function has(string $attribute): bool |
295
|
|
|
{ |
296
|
20 |
|
return is_array($this->data) ? array_key_exists($attribute, $this->data) : isset($this->data->$attribute); |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
/** |
300
|
|
|
* @psalm-return list< |
301
|
|
|
* array{ |
302
|
|
|
* label: string, |
303
|
|
|
* labelAttributes: array<array-key, mixed>, |
304
|
|
|
* labelTag: string, |
305
|
|
|
* value: string, |
306
|
|
|
* valueAttributes: array<array-key, mixed>, |
307
|
|
|
* valueTag: string, |
308
|
|
|
* } |
309
|
|
|
* > |
310
|
|
|
*/ |
311
|
23 |
|
private function normalizeColumns(array $fields): array |
312
|
|
|
{ |
313
|
23 |
|
$normalized = []; |
314
|
|
|
|
315
|
|
|
/** @psalm-var DataField[] $fields */ |
316
|
23 |
|
foreach ($fields as $field) { |
317
|
22 |
|
if ($field->getLabel() === '') { |
318
|
1 |
|
throw new InvalidArgumentException('The "attribute" or "label" must be set.'); |
319
|
|
|
} |
320
|
|
|
|
321
|
21 |
|
$labelAttributes = $field->getLabelAttributes() === [] |
322
|
21 |
|
? $this->labelAttributes : $field->getLabelAttributes(); |
323
|
21 |
|
$labelTag = $field->getLabelTag() === '' ? $this->labelTag : $field->getLabelTag(); |
324
|
21 |
|
$valueTag = $field->getValueTag() === '' ? $this->valueTag : $field->getValueTag(); |
325
|
21 |
|
$valueAttributes = $field->getValueAttributes() === [] |
326
|
21 |
|
? $this->valueAttributes : $field->getValueAttributes(); |
327
|
|
|
|
328
|
21 |
|
$normalized[] = [ |
329
|
21 |
|
'label' => Html::encode($field->getLabel()), |
330
|
21 |
|
'labelAttributes' => $this->renderAttributes($labelAttributes), |
331
|
21 |
|
'labelTag' => Html::encode($labelTag), |
332
|
21 |
|
'value' => Html::encodeAttribute($this->renderValue($field->getAttribute(), $field->getValue())), |
333
|
21 |
|
'valueAttributes' => $this->renderAttributes($valueAttributes), |
334
|
21 |
|
'valueTag' => Html::encode($valueTag), |
335
|
21 |
|
]; |
336
|
|
|
} |
337
|
|
|
|
338
|
21 |
|
return $normalized; |
339
|
|
|
} |
340
|
|
|
|
341
|
21 |
|
private function renderAttributes(array|Closure $attributes): array |
342
|
|
|
{ |
343
|
21 |
|
if ($attributes === []) { |
344
|
20 |
|
return []; |
345
|
|
|
} |
346
|
|
|
|
347
|
6 |
|
if ($attributes instanceof Closure) { |
|
|
|
|
348
|
1 |
|
return (array) $attributes($this->data); |
349
|
|
|
} |
350
|
|
|
|
351
|
5 |
|
return $attributes; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* @throws JsonException |
356
|
|
|
*/ |
357
|
23 |
|
private function renderItems(): string |
358
|
|
|
{ |
359
|
23 |
|
$fields = $this->normalizeColumns($this->fields); |
360
|
|
|
|
361
|
21 |
|
if ($fields === []) { |
362
|
1 |
|
return ''; |
363
|
|
|
} |
364
|
|
|
|
365
|
20 |
|
$rows = []; |
366
|
|
|
|
367
|
20 |
|
foreach ($fields as $field) { |
368
|
20 |
|
$label = strtr($this->labelTemplate, [ |
369
|
20 |
|
'{label}' => $field['label'], |
370
|
20 |
|
'{labelTag}' => $field['labelTag'], |
371
|
20 |
|
'{labelAttributes}' => Html::renderTagAttributes($field['labelAttributes']), |
372
|
20 |
|
]); |
373
|
|
|
|
374
|
20 |
|
$value = strtr($this->valueTemplate, [ |
375
|
20 |
|
'{value}' => $field['value'], |
376
|
20 |
|
'{valueTag}' => $field['valueTag'], |
377
|
20 |
|
'{valueAttributes}' => Html::renderTagAttributes($field['valueAttributes']), |
378
|
20 |
|
]); |
379
|
|
|
|
380
|
20 |
|
$rows[] = strtr($this->itemTemplate, [ |
381
|
20 |
|
'{dataAttributes}' => Html::renderTagAttributes($this->dataAttributes), |
382
|
20 |
|
'{label}' => $label, |
383
|
20 |
|
'{value}' => $value, |
384
|
20 |
|
]); |
385
|
|
|
} |
386
|
|
|
|
387
|
20 |
|
return implode(PHP_EOL, $rows); |
388
|
|
|
} |
389
|
|
|
|
390
|
21 |
|
private function renderValue(string $attribute, mixed $value): mixed |
391
|
|
|
{ |
392
|
21 |
|
if ($this->data === []) { |
393
|
1 |
|
throw new InvalidArgumentException('The "data" must be set.'); |
394
|
|
|
} |
395
|
|
|
|
396
|
20 |
|
if ($value === null && is_array($this->data) && $this->has($attribute)) { |
397
|
19 |
|
return match (is_bool($this->data[$attribute])) { |
398
|
19 |
|
true => $this->data[$attribute] ? $this->valueTrue : $this->valueFalse, |
399
|
19 |
|
default => $this->data[$attribute], |
400
|
19 |
|
}; |
401
|
|
|
} |
402
|
|
|
|
403
|
4 |
|
if ($value === null && is_object($this->data) && $this->has($attribute)) { |
404
|
2 |
|
return match (is_bool($this->data->{$attribute})) { |
405
|
2 |
|
true => $this->data->{$attribute} ? $this->valueTrue : $this->valueFalse, |
406
|
2 |
|
default => $this->data->{$attribute}, |
407
|
2 |
|
}; |
408
|
|
|
} |
409
|
|
|
|
410
|
3 |
|
if ($value instanceof Closure) { |
411
|
2 |
|
return $value($this->data); |
412
|
|
|
} |
413
|
|
|
|
414
|
1 |
|
return $value; |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
/** |
418
|
|
|
* Remove double spaces from string. |
419
|
|
|
* |
420
|
|
|
* @param string $string String to remove double spaces from. |
421
|
|
|
*/ |
422
|
20 |
|
private function removeDoubleLinesBreaks(string $string): string |
423
|
|
|
{ |
424
|
20 |
|
return preg_replace("/([\r\n]{4,}|[\n]{2,}|[\r]{2,})/", PHP_EOL, $string); |
425
|
|
|
} |
426
|
|
|
} |
427
|
|
|
|