Issues (13)

src/DetailView.php (1 issue)

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
$attributes is never a sub-type of Closure.
Loading history...
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