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) { |
|
0 ignored issues
–
show
introduced
by
![]() |
|||
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 |