Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.57% covered (success)
98.57%
69 / 70
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
PdfTemplate
98.55% covered (success)
98.55%
68 / 69
80.00% covered (warning)
80.00%
4 / 5
15
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromRow
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
5
 nullableNonEmptyString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 decodeJsonArray
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
5.12
 toArray
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
1
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 Aggregate Model.
13 *
14 * Encapsulates page geometry, header/footer markup, modular block definitions,
15 * and security metadata for PDF generation.
16 *
17 * @package App\Modules\Pdf\Domain\Model
18 */
19final readonly class PdfTemplate
20{
21    /**
22     * PdfTemplate constructor.
23     *
24     * @param int                  $id              Primary template ID.
25     * @param string               $code            Unique machine code.
26     * @param string               $name            Human-readable display name.
27     * @param string               $targetModule    Associated module (e.g. tickets, global).
28     * @param string               $category        Template category (e.g. document, invoice).
29     * @param string               $templateScope   Template scope ('record' for single record, 'list' for listing).
30     * @param string               $pageFormat      Page format size (A4, Letter, A3).
31     * @param string               $pageOrientation Page orientation (portrait, landscape).
32     * @param int                  $marginTop       Top margin in millimeters.
33     * @param int                  $marginBottom    Bottom margin in millimeters.
34     * @param int                  $marginLeft      Left margin in millimeters.
35     * @param int                  $marginRight     Right margin in millimeters.
36     * @param string|null          $headerHtml      Running page header HTML markup.
37     * @param string|null          $footerHtml      Running page footer HTML markup.
38     * @param string               $bodyHtml        Compiled HTML body content.
39     * @param array<string, mixed>|null $blocksJson Modular block tree configuration.
40     * @param string|null          $cssStyles       Custom stylesheet for paged media.
41     * @param string               $filenamePattern Generated PDF filename pattern.
42     * @param string|null          $watermarkText   Optional watermark text overlay.
43     * @param string               $languageCode    Locale code (pl, en).
44     * @param string|null          $description     Optional internal description notes.
45     * @param bool                 $isActive        Active publication status.
46     * @param int                  $recordStatus    Lifecycle state (1=active, 2=archived, 3=deleted).
47     * @param string|null          $createdAt       Creation timestamp string.
48     * @param string|null          $updatedAt       Update timestamp string.
49     * @param int                  $createdBy       Creator user ID.
50     * @param int                  $owner           Owner user ID.
51     * @param array<int>|null      $coOwners        Optional co-owner user IDs.
52     */
53    public function __construct(
54        public int $id,
55        public string $code,
56        public string $name,
57        public string $targetModule = 'global',
58        public string $category = 'document',
59        public string $templateScope = 'record',
60        public string $pageFormat = 'A4',
61        public string $pageOrientation = 'portrait',
62        public int $marginTop = 15,
63        public int $marginBottom = 15,
64        public int $marginLeft = 15,
65        public int $marginRight = 15,
66        public ?string $headerHtml = null,
67        public ?string $footerHtml = null,
68        public string $bodyHtml = '',
69        public ?array $blocksJson = null,
70        public ?string $cssStyles = null,
71        public string $filenamePattern = 'document_{{ record.id }}.pdf',
72        public ?string $watermarkText = null,
73        public string $languageCode = 'pl',
74        public ?string $description = null,
75        public bool $isActive = true,
76        public int $recordStatus = 1,
77        public ?string $createdAt = null,
78        public ?string $updatedAt = null,
79        public int $createdBy = 1,
80        public int $owner = 1,
81        public ?array $coOwners = null
82    ) {
83    }
84
85    /**
86     * Instantiates PdfTemplate from database associative record.
87     *
88     * @param array<string, mixed> $row Database associative row.
89     * @return self Populated PdfTemplate instance.
90     */
91    public static function fromRow(array $row): self
92    {
93        return new self(
94            id:              (int)($row['id'] ?? 0),
95            code:            (string)($row['code'] ?? ''),
96            name:            (string)($row['name'] ?? ''),
97            targetModule:    (string)($row['target_module'] ?? 'global'),
98            category:        (string)($row['category'] ?? 'document'),
99            templateScope:   (string)($row['template_scope'] ?? 'record'),
100            pageFormat:      (string)($row['page_format'] ?? 'A4'),
101            pageOrientation: (string)($row['page_orientation'] ?? 'portrait'),
102            marginTop:       (int)($row['margin_top'] ?? 15),
103            marginBottom:    (int)($row['margin_bottom'] ?? 15),
104            marginLeft:      (int)($row['margin_left'] ?? 15),
105            marginRight:     (int)($row['margin_right'] ?? 15),
106            headerHtml:      self::nullableNonEmptyString($row['header_html'] ?? null),
107            footerHtml:      self::nullableNonEmptyString($row['footer_html'] ?? null),
108            bodyHtml:        (string)($row['body_html'] ?? ''),
109            blocksJson:      self::decodeJsonArray($row['blocks_json'] ?? null),
110            cssStyles:       self::nullableNonEmptyString($row['css_styles'] ?? null),
111            filenamePattern: (string)($row['filename_pattern'] ?? 'document_{{ record.id }}.pdf'),
112            watermarkText:   self::nullableNonEmptyString($row['watermark_text'] ?? null),
113            languageCode:    (string)($row['language_code'] ?? 'pl'),
114            description:     self::nullableNonEmptyString($row['description'] ?? null),
115            isActive:        isset($row['is_active'])
116                ? (bool)$row['is_active']
117                : (($row['status'] ?? 'active') === 'active' && ((int)($row['special_access'] ?? 1) === 1)),
118            recordStatus:    (int)($row['special_access'] ?? $row['record_status'] ?? 1),
119            createdAt:       isset($row['created_at']) ? (string)$row['created_at'] : null,
120            updatedAt:       isset($row['updated_at']) ? (string)$row['updated_at'] : null,
121            createdBy:       (int)($row['created_by'] ?? 1),
122            owner:           (int)($row['owner'] ?? 1),
123            coOwners:        self::decodeJsonArray($row['co_owners'] ?? null)
124        );
125    }
126
127    private static function nullableNonEmptyString(mixed $val): ?string
128    {
129        return isset($val) && (string)$val !== '' ? (string)$val : null;
130    }
131
132    /**
133     * @return array<mixed>|null
134     */
135    private static function decodeJsonArray(mixed $val): ?array
136    {
137        if (is_array($val)) {
138            return $val;
139        }
140        if (is_string($val) && trim($val) !== '') {
141            $decoded = json_decode($val, true);
142            return is_array($decoded) ? $decoded : null;
143        }
144
145        return null;
146    }
147
148    /**
149     * Serializes aggregate to array for JSON responses and API output.
150     *
151     * @return array<string, mixed> Serialized dictionary.
152     */
153    public function toArray(): array
154    {
155        return [
156            'id'               => $this->id,
157            'code'             => $this->code,
158            'name'             => $this->name,
159            'target_module'    => $this->targetModule,
160            'category'         => $this->category,
161            'template_scope'   => $this->templateScope,
162            'page_format'      => $this->pageFormat,
163            'page_orientation' => $this->pageOrientation,
164            'margin_top'       => $this->marginTop,
165            'margin_bottom'    => $this->marginBottom,
166            'margin_left'      => $this->marginLeft,
167            'margin_right'     => $this->marginRight,
168            'header_html'      => $this->headerHtml,
169            'footer_html'      => $this->footerHtml,
170            'body_html'        => $this->bodyHtml,
171            'blocks_json'      => $this->blocksJson,
172            'css_styles'       => $this->cssStyles,
173            'filename_pattern' => $this->filenamePattern,
174            'watermark_text'   => $this->watermarkText,
175            'language_code'    => $this->languageCode,
176            'description'      => $this->description,
177            'is_active'        => $this->isActive,
178            'record_status'    => $this->recordStatus,
179            'created_at'       => $this->createdAt,
180            'updated_at'       => $this->updatedAt,
181            'created_by'       => $this->createdBy,
182            'owner'            => $this->owner,
183        ];
184    }
185}