Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.89% covered (success)
98.89%
89 / 90
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
TemplateCompilerService
98.88% covered (success)
98.88%
88 / 89
83.33% covered (warning)
83.33%
5 / 6
51
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 compileBlocks
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
8.04
 compileBlocksArray
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 render
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 compileBlockNode
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
35
 compileDefaultBlock
100.00% covered (success)
100.00%
4 / 4
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\Core\Template\Application\Service;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Template\Application\Service\BlockCompiler\TemplateLayoutBlockCompiler;
12use App\Core\Template\Application\Service\BlockCompiler\TemplateMediaBlockCompiler;
13use App\Core\Template\Application\Service\BlockCompiler\TemplateTableBlockCompiler;
14use App\Core\Template\Domain\Model\TemplateBlock;
15use Throwable;
16use Twig\Environment;
17use Twig\Loader\ArrayLoader;
18
19/**
20 * Visual Block AST Compiler and Template Rendering Service.
21 *
22 * Compiles modular blocks into robust HTML/Twig markup and executes
23 * template rendering in an isolated Twig sandbox. Delegates block
24 * compilation to specialized compiler subcomponents.
25 *
26 * @package App\Core\Template\Application\Service
27 */
28final class TemplateCompilerService
29{
30    private Environment $twig;
31    private TemplateLayoutBlockCompiler $layoutCompiler;
32    private TemplateTableBlockCompiler $tableCompiler;
33    private TemplateMediaBlockCompiler $mediaCompiler;
34
35    /**
36     * TemplateCompilerService constructor.
37     *
38     * @param TemplateLayoutBlockCompiler|null $layoutCompiler Layout block compiler.
39     * @param TemplateTableBlockCompiler|null $tableCompiler Table and summary block compiler.
40     * @param TemplateMediaBlockCompiler|null $mediaCompiler Media, QR, and signature block compiler.
41     */
42    public function __construct(
43        ?TemplateLayoutBlockCompiler $layoutCompiler = null,
44        ?TemplateTableBlockCompiler $tableCompiler = null,
45        ?TemplateMediaBlockCompiler $mediaCompiler = null,
46    ) {
47        $loader = new ArrayLoader();
48        $this->twig = new Environment($loader, [
49            'autoescape'       => 'html',
50            'strict_variables' => false,
51        ]);
52        $this->twig->addExtension(new TemplateTwigExtension());
53
54        $this->layoutCompiler = $layoutCompiler ?? new TemplateLayoutBlockCompiler();
55        $this->tableCompiler = $tableCompiler ?? new TemplateTableBlockCompiler();
56        $this->mediaCompiler = $mediaCompiler ?? new TemplateMediaBlockCompiler();
57    }
58
59    /**
60     * Compiles raw JSON blocks string or array into ready-to-render HTML/Twig markup.
61     *
62     * @param array<string, mixed>|string $blocksData Raw JSON string or deserialized block dictionary.
63     * @return string Compiled HTML/Twig string.
64     */
65    public function compileBlocks(array|string $blocksData): string
66    {
67        $blocksTree = is_string($blocksData)
68            ? (json_decode($blocksData, true) ?? [])
69            : $blocksData;
70
71        if (!is_array($blocksTree) || !isset($blocksTree['blocks']) || !is_array($blocksTree['blocks'])) {
72            return '';
73        }
74
75        $blocks = [];
76        foreach ($blocksTree['blocks'] as $rawBlock) {
77            if (is_array($rawBlock)) {
78                $blocks[] = TemplateBlock::fromArray($rawBlock);
79            }
80        }
81
82        if ($blocks === []) {
83            return '';
84        }
85
86        return $this->compileBlocksArray($blocks);
87    }
88
89    /**
90     * Compiles an array of TemplateBlock nodes into grid layout HTML.
91     *
92     * @param array<TemplateBlock> $blocks Block instances.
93     * @return string Compiled HTML.
94     */
95    private function compileBlocksArray(array $blocks): string
96    {
97        usort(
98            $blocks,
99            static fn (TemplateBlock $a, TemplateBlock $b): int => ($a->y <=> $b->y) ?: ($a->x <=> $b->x)
100        );
101
102        $output = '<div class="template-sheet-canvas template-grid-layout" '
103            . 'style="display:grid;grid-template-columns:repeat(12, 1fr);gap:12px;width:100%;">' . "\n";
104
105        foreach ($blocks as $block) {
106            $colSpan = max(1, min(12, $block->w));
107            $colStart = max(1, min(12, $block->x + 1));
108            $cellStyle = "grid-column:{$colStart} / span {$colSpan};width:100%;box-sizing:border-box;";
109
110            $output .= '<div class="template-grid-cell" style="' . $cellStyle . '">' . "\n"
111                . $this->compileBlockNode($block) . "\n"
112                . '</div>' . "\n";
113        }
114
115        $output .= '</div>';
116
117        return trim($output);
118    }
119
120    /**
121     * Executes Twig rendering with resolved context dictionary.
122     *
123     * @param string               $templateSource Raw HTML/Twig source code.
124     * @param array<string, mixed> $context        Hierarchical data variables.
125     * @return string Rendered final HTML string.
126     */
127    public function render(string $templateSource, array $context = []): string
128    {
129        try {
130            $templateName = 'tpl_' . hash('sha256', $templateSource);
131            $loader = new ArrayLoader([$templateName => $templateSource]);
132            $this->twig->setLoader($loader);
133
134            return $this->twig->render($templateName, $context);
135        } catch (Throwable $e) {
136            return '<div class="alert alert-danger">Template Render Error: '
137                . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8') . '</div>' . $templateSource;
138        }
139    }
140
141    /**
142     * Compiles a single TemplateBlock node into its corresponding HTML representation.
143     *
144     * @param TemplateBlock $block Target block instance.
145     * @return string HTML snippet.
146     */
147    private function compileBlockNode(TemplateBlock $block): string
148    {
149        return match ($block->type) {
150            // Structural Layout Primitives
151            'text_inline'           => $this->layoutCompiler->compileTextInline($block),
152            'text_block'            => $this->layoutCompiler->compileTextBlock($block),
153            'text_labeled'          => $this->layoutCompiler->compileTextLabeled($block),
154            'card_box'              => $this->layoutCompiler->compileCardBox($block),
155            'table_standard'        => $this->tableCompiler->compileTableStandard($block),
156            'table_summary'         => $this->tableCompiler->compileTableSummary($block),
157            'table_simple'          => $this->tableCompiler->compileTableSimple($block),
158            'image_box'             => $this->mediaCompiler->compileImageBox($block),
159            'code_barcode_qr'       => $this->mediaCompiler->compileBarcodeQr($block),
160            'kpi_metric'            => $this->tableCompiler->compileKpiMetric(
161                $block,
162                fn (string $k, string $f): string => $this->layoutCompiler->resolveValueExpr($k, $f)
163            ),
164            'divider_spacer'        => $this->layoutCompiler->compileDividerSpacer($block),
165            'signatures_box'        => $this->mediaCompiler->compileSignaturesBox($block),
166
167            // Legacy & Domain Mappings
168            'brand_header'          => $this->mediaCompiler->compileBrandHeader($block),
169            'rich_text_box'         => $this->layoutCompiler->compileRichTextBox($block),
170            'field_label'           => $this->layoutCompiler->compileFieldLabel($block),
171            'field_value'           => $this->layoutCompiler->compileFieldValue($block),
172            'field_pair'            => $this->layoutCompiler->compileFieldPair($block),
173            'records_table'         => $this->tableCompiler->compileRecordsTable($block),
174            'list_header'           => $this->tableCompiler->compileListHeader($block),
175            'list_summary'          => $this->tableCompiler->compileListSummary($block),
176            'list_kpi_bar'          => $this->tableCompiler->compileListKpiBar($block),
177            'key_value_grid'        => $this->layoutCompiler->compileKeyValueGrid($block),
178            'line_items_table'      => $this->tableCompiler->compileLineItemsTable($block),
179            'financial_summary_box' => $this->tableCompiler->compileFinancialSummary($block),
180            'tax_summary_box'       => $this->tableCompiler->compileTaxSummaryBox($block),
181            'bank_transfer_qr_box'  => $this->mediaCompiler->compileBankTransferQrBox($block),
182            'chart_sparkline'       => $this->mediaCompiler->compileChartSparklineBlock($block),
183            'verification_badge'    => $this->mediaCompiler->compileVerificationBadgeBlock($block),
184            'conditional_wrapper'   => $this->layoutCompiler->compileConditionalWrapper(
185                $block,
186                fn (TemplateBlock $child): string => $this->compileBlockNode($child)
187            ),
188            'cta_button'            => $this->layoutCompiler->compileCtaButton($block),
189            'page_break'            => '{{ page_break() }}',
190            'signature_block'       => $this->mediaCompiler->compileSignatureBlock($block),
191            'raw_html'              => (string)($block->props['content'] ?? ''),
192            default                 => $this->compileDefaultBlock($block),
193        };
194    }
195
196    /**
197     * Fallback for unknown block types.
198     *
199     * @param TemplateBlock $block Block instance.
200     * @return string Empty or child compiled string.
201     */
202    private function compileDefaultBlock(TemplateBlock $block): string
203    {
204        $childHtml = '';
205        foreach ($block->children as $child) {
206            $childHtml .= $this->compileBlockNode($child);
207        }
208
209        return $childHtml;
210    }
211}