Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
16.06% covered (danger)
16.06%
35 / 218
15.79% covered (danger)
15.79%
3 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Blocks
16.06% covered (danger)
16.06%
35 / 218
15.79% covered (danger)
15.79%
3 / 19
4989.54
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 __toString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 json
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 unwrap
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 image
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 images
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
210
 hasImage
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 excerpt
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
4.84
 columns
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 render
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
12
 isset
50.00% covered (danger)
50.00%
6 / 12
0.00% covered (danger)
0.00%
0 / 1
16.00
 renderValue
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
306
 getValueObject
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 renderImage
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 renderImages
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 prepareData
66.67% covered (warning)
66.67%
10 / 15
0.00% covered (danger)
0.00%
0 / 1
15.48
 blockValue
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
42
 mediaFieldData
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 mediaText
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Value;
6
7use Cosray\Assets\ResizeMode;
8use Cosray\Assets\Size;
9use Cosray\Field;
10use Cosray\Field\Capability\Translatable;
11use Cosray\Field\Owner;
12use Cosray\Util\Html as HtmlUtil;
13use Generator;
14use Gumlet\ImageResize;
15
16/**
17 * @property-read Field\Blocks&Translatable $field
18 */
19class Blocks extends Value
20{
21    protected readonly ?Generator $preparedData;
22
23    public function __construct(Owner $owner, Field\Blocks&Translatable $field, ValueContext $context)
24    {
25        parent::__construct($owner, $field, $context);
26
27        $this->preparedData = $this->prepareData($this->data);
28    }
29
30    public function __toString(): string
31    {
32        return $this->render();
33    }
34
35    public function json(): array
36    {
37        return $this->unwrap();
38    }
39
40    public function unwrap(): array
41    {
42        return [
43            'columns' => $this->columns(),
44            'data' => $this->preparedData,
45        ];
46    }
47
48    public function image(int $index = 1): ?Image
49    {
50        $i = 0;
51
52        foreach ($this->preparedData as $value) {
53            if ($value->type !== 'image') {
54                continue;
55            }
56
57            $i++;
58
59            if ($i === $index) {
60                return new Field\Image(
61                    $this->context->fieldName,
62                    $this->owner,
63                    new ValueContext($this->context->fieldName, $this->mediaFieldData($value->data)),
64                )
65                    ->limit(1)
66                    ->value();
67            }
68        }
69
70        return null;
71    }
72
73    public function images(bool $all = false): Generator
74    {
75        if ($all && $this->field->isTranslatable()) {
76            foreach ($this->data['value'] ?? [] as $data) {
77                if (!is_array($data)) {
78                    continue;
79                }
80
81                foreach ($data as $value) {
82                    if (!is_array($value)) {
83                        continue;
84                    }
85
86                    $item = new Block((string) ($value['type'] ?? ''), $value);
87
88                    if ($item->type === 'image') {
89                        yield new Field\Image(
90                            $this->context->fieldName,
91                            $this->owner,
92                            new ValueContext($this->context->fieldName, $this->mediaFieldData($item->data)),
93                        )
94                            ->limit(1)
95                            ->value();
96                    } elseif ($item->type === 'images') {
97                        foreach (new Field\Image(
98                            $this->context->fieldName,
99                            $this->owner,
100                            new ValueContext($this->context->fieldName, $this->mediaFieldData($item->data)),
101                        )->value() as $image) {
102                            yield $image;
103                        }
104                    }
105                }
106            }
107        } else {
108            foreach ($this->preparedData as $item) {
109                if ($item->type === 'image') {
110                    yield new Field\Image(
111                        $this->context->fieldName,
112                        $this->owner,
113                        new ValueContext($this->context->fieldName, $this->mediaFieldData($item->data)),
114                    )
115                        ->limit(1)
116                        ->value();
117                } elseif ($item->type === 'images') {
118                    foreach (new Field\Image(
119                        $this->context->fieldName,
120                        $this->owner,
121                        new ValueContext($this->context->fieldName, $this->mediaFieldData($item->data)),
122                    )->value() as $image) {
123                        yield $image;
124                    }
125                }
126            }
127        }
128    }
129
130    public function hasImage(int $index = 1): bool
131    {
132        $i = 0;
133
134        foreach ($this->preparedData as $value) {
135            if ($value->type !== 'image') {
136                continue;
137            }
138
139            $i++;
140
141            if ($i === $index) {
142                return true;
143            }
144        }
145
146        return false;
147    }
148
149    public function excerpt(
150        int $words = 30,
151        string $allowedTags = '',
152        int $index = 1,
153    ): string {
154        $i = 0;
155
156        foreach ($this->preparedData as $value) {
157            if ($value->type !== 'richtext') {
158                continue;
159            }
160
161            $i++;
162
163            if ($i === $index) {
164                return HtmlUtil::excerpt((string) $this->blockValue($value), $words, $allowedTags);
165            }
166        }
167
168        return '';
169    }
170
171    public function columns(): int
172    {
173        return (int) ($this->meta('columns', 12) ?: 12);
174    }
175
176    // Supported args:
177    //
178    // - prefix: All css classes are prefixed with this value. Default 'cms'
179    // - tag: The tag of the container. Default 'div'
180    // - maxImageWidth: The maximum width of images. Images will be resized according to colspan. Default: 1280
181    // - class: An additional class added to the container
182    public function render(mixed ...$args): string
183    {
184        $tag = $args['tag'] ?? 'div';
185        $args['tag'] = $tag;
186        $prefix = $args['prefix'] ?? 'cms';
187        $args['prefix'] = $prefix;
188        $class = $args['class'] ?? '';
189        $class = $class !== '' ? ' ' . $class : '';
190        $args['class'] = $class;
191
192        $columns = $this->columns();
193
194        $out =
195            '<'
196            . $tag
197            . ' class="'
198            . $prefix
199            . '-blocks '
200            . $prefix
201            . '-blocks-columns-'
202            . $columns
203            . $class
204            . '">';
205
206        foreach ($this->preparedData as $value) {
207            $out .= $this->renderValue($prefix, $value, $args);
208        }
209
210        $out .= '</' . $tag . '>';
211
212        return $out;
213    }
214
215    public function isset(): bool
216    {
217        if ($this->preparedData === null) {
218            return false;
219        }
220
221        $value = $this->data['value'] ?? null;
222
223        if (!is_array($value)) {
224            return false;
225        }
226
227        if ($this->field->isTranslatable()) {
228            $defaultValue = $value[$this->defaultLocale->id] ?? [];
229
230            return is_array($defaultValue) && count($defaultValue) > 0;
231        }
232
233        if (!isset($this->data['type']) && array_is_list($value)) {
234            return count($value) > 0;
235        }
236
237        $defaultValue = $value[Field\Field::NEUTRAL_LOCALE] ?? [];
238
239        return is_array($defaultValue) && count($defaultValue) > 0;
240    }
241
242    protected function renderValue(string $prefix, Block $value, array $args): string
243    {
244        $colspan = $prefix . '-colspan-' . $value->data['colspan'];
245        $rowspan = $prefix . '-rowspan-' . $value->data['rowspan'];
246        $colstart = $value->data['colstart'] ?? null
247            ? $prefix . '-colstart-' . $value->data['colstart']
248            : null;
249        $styleClass = $value->styleClass();
250        $class = $styleClass ? ' ' . $styleClass : '';
251
252        $out =
253            '<div class="'
254            . $prefix
255            . '-'
256            . $value->type
257            . ' '
258            . $colspan
259            . ' '
260            . $rowspan
261            . ($colstart ? ' ' . $colstart : '')
262            . $class
263            . '">';
264        $blockValue = $this->blockValue($value);
265        $out .= match ($value->type) {
266            'richtext' => $blockValue,
267            'text' => $blockValue,
268            'h1' => '<h1>' . $blockValue . '</h1>',
269            'h2' => '<h2>' . $blockValue . '</h2>',
270            'h3' => '<h3>' . $blockValue . '</h3>',
271            'h4' => '<h4>' . $blockValue . '</h4>',
272            'h5' => '<h5>' . $blockValue . '</h5>',
273            'h6' => '<h6>' . $blockValue . '</h6>',
274            'iframe' => $blockValue,
275            'image' => $this->renderImage($value->data, $args),
276            'images' => $this->renderImages($value->data),
277            'youtube' => $this->getValueObject(Field\Youtube::class, $value)->__toString(),
278            'video' => $this->getValueObject(
279                Field\Video::class,
280                new Block($value->type, $this->mediaFieldData($value->data)),
281            )->__toString(),
282        };
283        $out .= '</div>';
284
285        return $out;
286    }
287
288    protected function getValueObject(string $class, Block $item): Value
289    {
290        return new $class(
291            $this->context->fieldName,
292            $this->owner,
293            new ValueContext($this->context->fieldName, $item->data),
294        )->value();
295    }
296
297    protected function renderImage(array $data, array $args): string
298    {
299        $file = (string) ($data['value'][0]['file'] ?? '');
300        $title = $this->mediaText($data['value'][0] ?? [], 'title') ?: $this->mediaText(
301            $data['value'][0] ?? [],
302            'alt',
303        );
304        $maxWidth = $args['maxImageWidth'] ?? 1440;
305        $path = $this->assetsPath() . $file;
306        $image = $this->getAssets()->image($path);
307        $resized = $image->resize(
308            new Size((int) ($maxWidth / $this->columns()) * (int) ($data['colspan'] ?? 12)),
309            ResizeMode::Width,
310            enlarge: false,
311            quality: null,
312        );
313        $url = $resized->url(true);
314
315        return "<img src=\"{$url}\" alt=\"{$title}\" data-path-original=\"{$path}\">";
316    }
317
318    protected function renderImages(array $data): string
319    {
320        $result = '';
321
322        foreach ($data['value'] ?? [] as $f) {
323            $file = (string) ($f['file'] ?? '');
324            $title = $this->mediaText($f, 'title') ?: $this->mediaText($f, 'alt');
325            $path = $this->assetsPath() . $file;
326            $image = $this->getAssets()->image($path);
327            $resized = $image->resize(
328                new Size(400, 267, cropMode: ImageResize::CROPCENTER),
329                ResizeMode::Crop,
330                enlarge: false,
331                quality: null,
332            );
333            $url = $resized->url(true);
334
335            $result .= "<div class=\"cms-blocks-images-image\"><img src=\"{$url}\" alt=\"{$title}\" data-path-original=\"{$path}\"></div>";
336        }
337
338        if ($result) {
339            return '<div class="cms-blocks-images">' . $result . '</div>';
340        }
341
342        return '';
343    }
344
345    protected function prepareData(array $data): Generator
346    {
347        $fields = [];
348
349        if ($this->field->isTranslatable()) {
350            $value = $this->effective($data['value'] ?? []);
351            $fields = is_array($value) ? $value : [];
352        } elseif (
353            !isset($data['type'])
354            && isset($data['value'])
355            && is_array($data['value'])
356            && array_is_list($data['value'])
357        ) {
358            $fields = $data['value'];
359        } else {
360            $value = $data['value'][Field\Field::NEUTRAL_LOCALE] ?? [];
361            $fields = is_array($value) ? $value : [];
362        }
363
364        foreach ($fields as $field) {
365            if (!is_array($field) || !is_string($field['type'] ?? null)) {
366                continue;
367            }
368
369            yield new Block($field['type'], $field);
370        }
371    }
372
373    private function blockValue(Block $block): string
374    {
375        $value = $block->data['value'] ?? [];
376
377        if (!is_array($value)) {
378            return is_string($value) || is_numeric($value) ? (string) $value : '';
379        }
380
381        $value = $this->effective($value);
382
383        return is_string($value) || is_numeric($value) ? (string) $value : '';
384    }
385
386    private function mediaFieldData(array $data): array
387    {
388        $data['value'] = [Field\Field::NEUTRAL_LOCALE => $data['value'] ?? $data['files'] ?? []];
389
390        return $data;
391    }
392
393    private function mediaText(array $item, string $key): string
394    {
395        $value = $item['meta'][$key] ?? $item[$key] ?? [];
396
397        if (!is_array($value)) {
398            return is_string($value) || is_numeric($value) ? (string) $value : '';
399        }
400
401        $value = $this->effective($value);
402
403        return is_string($value) || is_numeric($value) ? (string) $value : '';
404    }
405}