Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
13 / 13
CRAP
100.00% covered (success)
100.00%
1 / 1
Block
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
13 / 13
21
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
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 uid
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 handle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 layout
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 meta
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 json
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 unwrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __get
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 render
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderWith
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
4
 fieldValues
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Value;
6
7use Cosray\Block\Layout;
8use Cosray\Block\RenderContext;
9use Cosray\Contract\Block as BlockType;
10use Cosray\Exception\NoSuchProperty;
11use Cosray\Field;
12
13use function Cosray\escape;
14
15/**
16 * One row of a Blocks field: its type, grid placement, block meta and
17 * the type's fields, reachable as properties like on an entry.
18 *
19 * @property-read Field\Blocks $field
20 */
21class Block extends Value
22{
23    /** @var array<string, Field\Field> */
24    protected array $fields = [];
25
26    protected readonly Layout $layout;
27
28    public function __construct(
29        Field\Owner $owner,
30        Field\Blocks $field,
31        ValueContext $context,
32        public readonly string $type,
33    ) {
34        parent::__construct($owner, $field, $context);
35
36        $data = $this->data['fields'] ?? [];
37        $this->fields = $field->blockFieldsFor($type, is_array($data) ? $data : []);
38        $this->layout = Layout::normalize($this->data['layout'] ?? null, $field->getColumns(), $field->getMin());
39    }
40
41    public function __toString(): string
42    {
43        return $this->render();
44    }
45
46    public function uid(): ?string
47    {
48        $uid = $this->data['uid'] ?? null;
49
50        return is_string($uid) ? $uid : null;
51    }
52
53    /** The type's `data-type` value. */
54    public function handle(): string
55    {
56        return $this->field->blockHandle($this->type);
57    }
58
59    public function layout(): Layout
60    {
61        return $this->layout;
62    }
63
64    /** A block-level meta entry, such as `class` or `id`. */
65    public function meta(string $key, mixed $default = null): mixed
66    {
67        return parent::meta($key, $default);
68    }
69
70    public function json(): array
71    {
72        return $this->fieldValues(static fn(Value $value): mixed => $value->json());
73    }
74
75    public function unwrap(): array
76    {
77        return $this->fieldValues(static fn(Value $value): mixed => $value->unwrap());
78    }
79
80    public function isset(): bool
81    {
82        return count($this->fields) > 0;
83    }
84
85    public function __get(string $name): mixed
86    {
87        if (isset($this->fields[$name])) {
88            return $this->fields[$name]->value();
89        }
90
91        throw new NoSuchProperty("Block doesn't have field '{$name}'");
92    }
93
94    /**
95     * This block alone, wrapper element included. Supports the same
96     * arguments as `Blocks::render()`.
97     */
98    public function render(mixed ...$args): string
99    {
100        $ctx = new RenderContext($this->owner, $this->fieldName, $this->field->getColumns(), $args);
101
102        return $this->renderWith($ctx, $this->field->services()->blocks->create($this->type, $this->owner));
103    }
104
105    /**
106     * The rendering contract: `{prefix}-block` plus the meta class, the
107     * meta id, `data-type`, the layout as data attributes and custom
108     * properties, then the type's output. An empty output emits no
109     * element, so a block whose asset is gone leaves no grid cell.
110     */
111    public function renderWith(RenderContext $ctx, BlockType $type): string
112    {
113        $inner = $type->render($this, $ctx);
114
115        if ($inner === '') {
116            return '';
117        }
118
119        $class = $ctx->prefix() . '-block';
120        $styleClass = $this->styleClass();
121
122        if ($styleClass !== null) {
123            $class .= ' ' . $styleClass;
124        }
125
126        $attributes = ' class="' . escape($class) . '"';
127        $id = $this->elementId();
128
129        if ($id !== null) {
130            $attributes .= ' id="' . escape($id) . '"';
131        }
132
133        $layout = $this->layout;
134        // The columns the block takes out of its row: the reference sheet
135        // spans them and pushes the box past the indent. Derived, but
136        // emitted so CSS that cannot read the inline style still has it.
137        $reserved = $layout->indent + $layout->colspan;
138        $attributes .=
139            ' data-type="'
140            . escape($this->handle())
141            . "\" data-colspan=\"{$layout->colspan}\" data-rowspan=\"{$layout->rowspan}\" data-indent=\"{$layout->indent}\""
142            . " data-reserved=\"{$reserved}\""
143            . " style=\"--colspan: {$layout->colspan}; --rowspan: {$layout->rowspan}; --indent: {$layout->indent};"
144            . " --reserved: {$reserved}\"";
145
146        return "<div{$attributes}>{$inner}</div>";
147    }
148
149    /**
150     * @param callable(Value): mixed $resolve
151     * @return array{uid: ?string, type: string, handle: string, layout: array, fields: array<string, mixed>, meta: array}
152     */
153    private function fieldValues(callable $resolve): array
154    {
155        $fields = [];
156
157        foreach ($this->fields as $name => $field) {
158            $fields[$name] = $resolve($field->value());
159        }
160
161        $meta = $this->data['meta'] ?? [];
162
163        return [
164            'uid' => $this->uid(),
165            'type' => $this->type,
166            'handle' => $this->handle(),
167            'layout' => $this->layout->array(),
168            'fields' => $fields,
169            'meta' => is_array($meta) ? $meta : [],
170        ];
171    }
172}