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    /** @param array<mixed> $args */
26    public function __construct(
27        public readonly Owner $owner,
28        public readonly string $fieldName,
29        public readonly int $columns,
30        public readonly array $args,
31    ) {
32        $this->tag = $this->name('tag', 'div');
33        $this->prefix = $this->name('prefix', 'cms');
34        $class = $args['class'] ?? '';
35        $this->class = is_string($class) ? trim($class) : '';
36    }
37
38    /** The container element's tag. */
39    public function tag(): string
40    {
41        return $this->tag;
42    }
43
44    /** Every generated class name starts with this. */
45    public function prefix(): string
46    {
47        return $this->prefix;
48    }
49
50    /** The extra container class from the `class` argument, unescaped. */
51    public function class(): string
52    {
53        return $this->class;
54    }
55
56    /**
57     * Resolve a locale map along the locale fallback chain.
58     *
59     * @param array<mixed> $map
60     */
61    public function effective(array $map): mixed
62    {
63        $locale = $this->owner->locale();
64
65        while ($locale) {
66            if ($this->filled($map[$locale->id] ?? null)) {
67                return $map[$locale->id];
68            }
69
70            $locale = $locale->fallback();
71        }
72
73        if ($this->filled($map[Field::NEUTRAL_LOCALE] ?? null)) {
74            return $map[Field::NEUTRAL_LOCALE];
75        }
76
77        return null;
78    }
79
80    /** The catalog asset a media item references, if it exists. */
81    public function asset(string $uid): ?Asset
82    {
83        return $uid === '' ? null : $this->owner->assets()->get($uid);
84    }
85
86    private function name(string $key, string $default): string
87    {
88        $value = $this->args[$key] ?? $default;
89
90        if (!is_string($value) || !preg_match(self::NAME, $value)) {
91            throw new RuntimeException(
92                "Blocks error: `{$key}` must be a plain name (letters, digits and dashes, starting with a letter)",
93            );
94        }
95
96        return $value;
97    }
98
99    private function filled(mixed $value): bool
100    {
101        return $value !== null && $value !== '' && $value !== [];
102    }
103}