Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
PdfBlock
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
7 / 7
15
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
 fromArray
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 resolveId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 resolveType
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 resolveArrayProperty
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 parseChildren
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 toArray
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Pdf\Domain\Model;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11/**
12 * Immutable PDF Template Block Domain Model.
13 *
14 * Encapsulates a printable A4 paged component with 2D grid coordinates (x, y, w, h),
15 * designed strictly for PDF rendering engines (mPDF, Chromium).
16 *
17 * @package App\Modules\Pdf\Domain\Model
18 */
19final readonly class PdfBlock
20{
21    /**
22     * PdfBlock constructor.
23     *
24     * @param string                $id       Unique block node ID (e.g. pdf_blk_1a2b).
25     * @param string                $type     PDF-specific block type (starts with pdf_).
26     * @param int                   $x        Horizontal grid position (0 to 11).
27     * @param int                   $y        Vertical row position.
28     * @param int                   $w        Width in grid columns (1 to 12).
29     * @param int                   $h        Height in grid units.
30     * @param array<string, mixed>  $props    Block configuration properties.
31     * @param array<int, self>      $children Nested child blocks.
32     * @param array<string, string> $styles   Custom CSS styling rules.
33     */
34    public function __construct(
35        public string $id,
36        public string $type,
37        public int $x = 0,
38        public int $y = 0,
39        public int $w = 12,
40        public int $h = 2,
41        public array $props = [],
42        public array $children = [],
43        public array $styles = []
44    ) {
45    }
46
47    /**
48     * Instantiates PdfBlock from array dictionary.
49     *
50     * @param array<string, mixed> $data Serialized block dictionary.
51     * @return self Populated PdfBlock instance.
52     */
53    public static function fromArray(array $data): self
54    {
55        $id = self::resolveId($data['id'] ?? null);
56        $type = self::resolveType($data['type'] ?? 'pdf_text_block');
57        $x = max(0, (int)($data['x'] ?? 0));
58        $y = max(0, (int)($data['y'] ?? 0));
59        $w = max(1, min(12, (int)($data['w'] ?? 12)));
60        $h = max(1, (int)($data['h'] ?? 2));
61        $props = self::resolveArrayProperty($data['props'] ?? null);
62        $styles = self::resolveArrayProperty($data['styles'] ?? null);
63        $children = self::parseChildren($data['children'] ?? null);
64
65        return new self($id, $type, $x, $y, $w, $h, $props, $children, $styles);
66    }
67
68    /**
69     * Resolves block identifier or generates a unique fallback.
70     *
71     * @param mixed $id Raw id value.
72     * @return string Unique block id.
73     */
74    private static function resolveId(mixed $id): string
75    {
76        if ($id !== null && (string)$id !== '') {
77            return (string)$id;
78        }
79        return 'pdf_' . bin2hex(random_bytes(4));
80    }
81
82    /**
83     * Resolves block type ensuring proper pdf prefix.
84     *
85     * @param mixed $rawType Raw block type.
86     * @return string Normalized type.
87     */
88    private static function resolveType(mixed $rawType): string
89    {
90        $typeStr = (string)$rawType;
91        if (str_starts_with($typeStr, 'pdf_')) {
92            return $typeStr;
93        }
94        return 'pdf_' . $typeStr;
95    }
96
97    /**
98     * Resolves an array property, returning empty array on non-array.
99     *
100     * @param mixed $prop Raw property value.
101     * @return array<string, mixed> Safe array dictionary.
102     */
103    private static function resolveArrayProperty(mixed $prop): array
104    {
105        if (is_array($prop)) {
106            return $prop;
107        }
108        return [];
109    }
110
111    /**
112     * Parses recursive child blocks.
113     *
114     * @param mixed $childrenData Raw children array.
115     * @return array<int, self> List of PdfBlock instances.
116     */
117    private static function parseChildren(mixed $childrenData): array
118    {
119        if (!is_array($childrenData)) {
120            return [];
121        }
122        $children = [];
123        foreach ($childrenData as $childData) {
124            if (is_array($childData)) {
125                $children[] = self::fromArray($childData);
126            }
127        }
128        return $children;
129    }
130
131    /**
132     * Serializes block to associative array.
133     *
134     * @return array<string, mixed> Serialized block tree.
135     */
136    public function toArray(): array
137    {
138        $serializedChildren = [];
139        foreach ($this->children as $child) {
140            $serializedChildren[] = $child->toArray();
141        }
142
143        return [
144            'id'       => $this->id,
145            'type'     => $this->type,
146            'x'        => $this->x,
147            'y'        => $this->y,
148            'w'        => $this->w,
149            'h'        => $this->h,
150            'props'    => $this->props,
151            'children' => $serializedChildren,
152            'styles'   => $this->styles,
153        ];
154    }
155}