$placeholders */ public function render(EmailTextKey $key, array $placeholders): RenderedEmailTextDto { $definition = $this->catalog->get($key); $emailText = $this->emailTextRepository->findByKey($key); return $this->renderValues( $key, $emailText?->getSubject() ?: $definition->defaultSubject, $emailText?->getHeadline() ?? $definition->defaultHeadline, $emailText?->getBody() ?: $definition->defaultBody, $placeholders ); } /** * Renders wording that is not (or not yet) the saved one - the admin preview shows * what is currently in the form, which is the whole point of previewing before saving. * The values come straight from a half-filled form, so all three may be null. * * @param array $placeholders */ public function renderValues( EmailTextKey $key, ?string $subject, ?string $headline, ?string $body, array $placeholders, ): RenderedEmailTextDto { $definition = $this->catalog->get($key); // Three token maps from the same values, because the three parts of a mail are not // the same kind of text: the subject is a plain header, the headline is escaped // plain text, and the body is Markdown the renderer escapes for us. $rawTokens = []; $htmlTokens = []; foreach ($definition->getPlaceholderNames() as $name) { $value = (string) ($placeholders[$name] ?? ''); $value = '' === trim($value) ? self::EMPTY_VALUE : $value; $rawTokens['{'.$name.'}'] = $value; $htmlTokens['{'.$name.'}'] = htmlspecialchars($value, self::ESCAPE_FLAGS, 'UTF-8'); } return new RenderedEmailTextDto( strtr($subject ?? '', $rawTokens), $this->renderHeadline($headline ?? '', $htmlTokens), $this->mailBodyRenderer->render($this->insertBookingUrl($body ?? ''), $rawTokens), ); } /** * Refuses rather than sending a broken link: an unconfigured APPLICATION_FORM_BOOKING_URL * would otherwise render as and invite an applicant to book a call they cannot * reach. The caller decides what that means - the endpoint logs it and answers the * submission anyway, a reviewer sending by hand gets to see it. */ private function insertBookingUrl(string $body): string { if (false === str_contains($body, self::BOOKING_URL_TOKEN)) { return $body; } if ('' === trim($this->bookingUrl)) { throw new \RuntimeException('The mail wording uses {bookingUrl}, but APPLICATION_FORM_BOOKING_URL is not configured.'); } return str_replace(self::BOOKING_URL_TOKEN, trim($this->bookingUrl), $body); } /** * The headline is set as the mail's own

, so it deliberately takes no Markdown: * emphasis inside an already bold heading says nothing, and a stray "-" or "#" at the * start of one should stay the character it is. * * @param array $tokens */ private function renderHeadline(string $headline, array $tokens): string { return strtr( htmlspecialchars(trim($headline), self::ESCAPE_FLAGS, 'UTF-8'), $tokens ); } }