*/ private array $blocks; /** * @param array $overrides Blockinhalte, die den gespeicherten Stand ersetzen -- für die * Vorschau in der Vorlagen-Verwaltung, damit ungespeicherte * Änderungen sichtbar werden. */ public function __construct(private readonly string $documentType, array $overrides = []) { $stored = DocumentTemplate::forType($this->documentType) ->map(fn(DocumentTemplate $block): string => (string) $block->content) ->all(); $this->blocks = array_merge($stored, array_map(strval(...), $overrides)); } /** * @param array $tokens Platzhalter ohne geschweifte Klammern, z.B. ['invoice_number' => '...'] */ public function render(array $tokens): string { $html = $this->blockContent(DocumentTemplate::BLOCK_LAYOUT); $html = $this->insertBlocks($html); $html = $this->insertAssets($html); $html = $this->applyConditionals($html, $tokens); $html = $this->insertTokens($html, $tokens); $style = $this->stripBareDataUris($this->insertTokens( $this->insertAssets($this->blockContent(DocumentTemplate::BLOCK_STYLE)), $tokens )); return view('pdfs.document', [ 'title' => $tokens['document_title'] ?? '', 'style' => $style, 'content' => $html, ])->render(); } /** * Entfernt Data-URIs, die nicht in `url(...)` bzw. Anführungszeichen stehen. * * dompdf lagert Data-URIs im CSS nur dann in interne Blobs aus, wenn ihnen `(`, `"` oder `'` * vorausgeht (siehe Stylesheet::_parse_css). Ein bar im CSS stehendes Data-URI bleibt als Rohtext * liegen, und die anschließende Ruleset-Regex `[^{]*{[^}]*}` scannt für jede Startposition durch * das gesamte Base64 -- quadratische Laufzeit. Bei einem eingebetteten Logo sind das Minuten, und * unter PHP-FPM stirbt der Worker am Zeitlimit: die Anfrage endet in einem 502. * * Gültiges CSS ist so ein Data-URI ohnehin nicht. Es wird deshalb entfernt und weitergerendert, * statt das Dokument scheitern zu lassen -- wer eine Rechnung braucht, hat die Vorlage nicht * kaputt gemacht. Die Warnung im Log sorgt dafür, dass der Zustand trotzdem auffällt. */ private function stripBareDataUris(string $css): string { // Das Semikolon gehört zum Data-URI selbst ("data:image/png;base64,..."), darf hier also nicht // begrenzen -- sonst bliebe der Base64-Rumpf stehen und der Parser bremst weiter. $cleaned = preg_replace('/(? $this->documentType, 'block' => DocumentTemplate::BLOCK_STYLE, ]); } return $cleaned; } /** Ersetzt `{block:name}` durch den jeweiligen Blockinhalt. Leere oder fehlende Blöcke fallen weg. */ private function insertBlocks(string $html): string { return preg_replace_callback( '/\{block:([a-z0-9_-]+)}/i', fn(array $match): string => $this->blockContent($match[1]), $html ); } /** Ersetzt `{asset:name}` durch die Data-URI des Bildes. Unbekannte Namen werden zu einem Leerstring. */ private function insertAssets(string $html): string { if (!str_contains($html, '{asset:')) { return $html; } $assets = DocumentAsset::all()->keyBy('name'); return preg_replace_callback( '/\{asset:([a-z0-9_-]+)}/i', static fn(array $match): string => $assets->get($match[1])?->toDataUri() ?? '', $html ); } /** * Wertet `{if:token}...{/if:token}` aus: der Abschnitt bleibt nur stehen, wenn der Platzhalter einen * Wert hat. Ohne das stünden auf der Rechnung leere Zeilen, hängende Trenner und Beschriftungen ohne * Wert ("Steuernummer: "), sobald ein optionales Feld beim Tenant nicht gepflegt ist. * * Verschachtelte Bedingungen werden nicht unterstützt -- für die Vorlagen hier reicht eine Ebene. * * @param array $tokens */ private function applyConditionals(string $html, array $tokens): string { return preg_replace_callback( '/\{if:([a-z0-9_]+)}(.*?)\{\/if:\1}/is', static fn(array $match): string => trim((string) ($tokens[$match[1]] ?? '')) === '' ? '' : $match[2], $html ); } /** * Ersetzt die Wert-Platzhalter. Unbekannte Platzhalter bleiben unangetastet stehen -- das macht beim * Pflegen der Vorlage sofort sichtbar, dass ein Name nicht stimmt. * * @param array $tokens */ private function insertTokens(string $html, array $tokens): string { $replacements = []; foreach ($tokens as $name => $value) { $replacements['{' . $name . '}'] = (string) $value; } return strtr($html, $replacements); } private function blockContent(string $block): string { return $this->blocks[$block] ?? ''; } }