Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
Layout
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
7 / 7
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 normalize
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 placed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 at
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 array
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 int
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 clamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Block;
6
7/**
8 * A block's grid placement: the columns and rows it spans and, for a
9 * block on the field's grid, the column and row it starts at (1-based).
10 * A block without a position gets one from Placement; the blocks of a
11 * split have none, they are laid out on the split's area in order: its
12 * colspan is their column count, its rowspan their row limit. Readers
13 * clamp stored values so a field narrowed later or an out-of-range
14 * import never breaks a render; the same bounds are enforced on save.
15 */
16final readonly class Layout
17{
18    public const int MAX_ROWSPAN = 6;
19
20    public function __construct(
21        public int $colspan,
22        public int $rowspan,
23        public int $col = 0,
24        public int $row = 0,
25    ) {}
26
27    public static function normalize(mixed $layout, int $columns, int $min, int $rows = self::MAX_ROWSPAN): self
28    {
29        $layout = is_array($layout) ? $layout : [];
30        $colspan = self::clamp(self::int($layout['colspan'] ?? null, $columns), min($min, $columns), $columns);
31        $rowspan = self::clamp(self::int($layout['rowspan'] ?? null, 1), 1, $rows);
32        $col = self::int($layout['col'] ?? null, 0);
33        $row = self::int($layout['row'] ?? null, 0);
34
35        if ($col < 1 || $row < 1) {
36            return new self($colspan, $rowspan);
37        }
38
39        return new self($colspan, $rowspan, self::clamp($col, 1, $columns - $colspan + 1), $row);
40    }
41
42    public function placed(): bool
43    {
44        return $this->col > 0;
45    }
46
47    public function at(int $col, int $row): self
48    {
49        return new self($this->colspan, $this->rowspan, $col, $row);
50    }
51
52    /** @return array{colspan: int, rowspan: int, col?: int, row?: int} */
53    public function array(): array
54    {
55        $layout = ['colspan' => $this->colspan, 'rowspan' => $this->rowspan];
56
57        return $this->placed() ? [...$layout, 'col' => $this->col, 'row' => $this->row] : $layout;
58    }
59
60    private static function int(mixed $value, int $default): int
61    {
62        return is_int($value) || is_numeric($value) ? (int) $value : $default;
63    }
64
65    private static function clamp(int $value, int $min, int $max): int
66    {
67        return max($min, min($max, $value));
68    }
69}