Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
RenderContext
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
8 / 8
17
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 tag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 prefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 class
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 effective
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 asset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 name
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 filled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Block;
6
7use Cosray\Assets\Asset;
8use Cosray\Exception\RuntimeException;
9use Cosray\Field\Field;
10use Cosray\Field\Owner;
11
12/**
13 * What a block type needs to render itself on the frontend site, plus
14 * the render arguments (`tag`, `prefix`, `class`, and whatever the
15 * types read, like `imageSizes`) validated once per render.
16 */
17final class RenderContext
18{
19    private const string NAME = '/^[a-z][a-z0-9-]*$/i';
20
21    private readonly string $tag;
22    private readonly string $prefix;
23    private readonly string $class;
24
25    public function __construct(
26        public readonly Owner $owner,
27        public readonly string $fieldName,
28        public readonly int $columns,
29        public readonly array $args,
30    ) {
31        $this->tag = $this->name('tag', 'div');
32        $this->prefix = $this->name('prefix', 'cms');
33        $class = $args['class'] ?? '';
34        $this->class = is_string($class) ? trim($class) : '';
35    }
36
37    /** The container element's tag. */
38    public function tag(): string
39    {
40        return $this->tag;
41    }
42
43    /** Every generated class name starts with this. */
44    public function prefix(): string
45    {
46        return $this->prefix;
47    }
48
49    /** The extra container class from the `class` argument, unescaped. */
50    public function class(): string
51    {
52        return $this->class;
53    }
54
55    /**
56     * Resolve a locale map along the locale fallback chain.
57     */
58    public function effective(array $map): mixed
59    {
60        $locale = $this->owner->locale();
61
62        while ($locale) {
63            if ($this->filled($map[$locale->id] ?? null)) {
64                return $map[$locale->id];
65            }
66
67            $locale = $locale->fallback();
68        }
69
70        if ($this->filled($map[Field::NEUTRAL_LOCALE] ?? null)) {
71            return $map[Field::NEUTRAL_LOCALE];
72        }
73
74        return null;
75    }
76
77    /** The catalog asset a media item references, if it exists. */
78    public function asset(string $uid): ?Asset
79    {
80        return $uid === '' ? null : $this->owner->assets()->get($uid);
81    }
82
83    private function name(string $key, string $default): string
84    {
85        $value = $this->args[$key] ?? $default;
86
87        if (!is_string($value) || !preg_match(self::NAME, $value)) {
88            throw new RuntimeException(
89                "Blocks error: `{$key}` must be a plain name (letters, digits and dashes, starting with a letter)",
90            );
91        }
92
93        return $value;
94    }
95
96    private function filled(mixed $value): bool
97    {
98        return $value !== null && $value !== '' && $value !== [];
99    }
100}