Files
myep/src/Service/AccommodationBookingPdfGenerator.php
T

119 lines
4.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Service;
use App\Email\PdfAttachment;
use App\Entity\Groups\AccommodationBooking;
use Dompdf\Dompdf;
use Dompdf\Options;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\String\Slugger\AsciiSlugger;
use Twig\Environment;
/**
* Renders a one-page A4 booking document for a group accommodation booking.
*
* The document repeats the data of the admin detail page and includes the very same
* price breakdown partial the booking emails use, so all three stay in sync.
*/
class AccommodationBookingPdfGenerator
{
private const TEMPLATE = 'pdf/accommodation_booking.html.twig';
private const LOGO_PATH = '/assets/images/logo.png';
public function __construct(
private readonly Environment $twig,
private readonly AccommodationBookingBreakdownCalculator $breakdownCalculator,
#[Autowire('%kernel.project_dir%')]
private readonly string $projectDir,
) {
}
/**
* @throws \RuntimeException when the booking carries no price information
*/
public function render(AccommodationBooking $booking): string
{
$breakdown = $this->breakdownCalculator->compute($booking);
if (null === $breakdown) {
throw new \RuntimeException('Für diese Buchung liegt keine Preisaufschlüsselung vor.');
}
$html = $this->twig->render(self::TEMPLATE, [
'booking' => $booking,
'priceBreakdown' => $breakdown,
'currency' => $booking->getPricingCurrency() ?? $breakdown['currency'],
'logoSrc' => $this->logoDataUri(),
]);
$options = new Options();
// DejaVu Sans ships with dompdf and covers umlauts and the currency symbols
// emitted by format_currency.
$options->setDefaultFont('DejaVu Sans');
$options->setDefaultPaperSize('A4');
$options->setDefaultPaperOrientation('portrait');
$options->setIsRemoteEnabled(false);
$dompdf = new Dompdf($options);
$dompdf->loadHtml($html, 'UTF-8');
$dompdf->render();
return (string) $dompdf->output();
}
/**
* @throws \RuntimeException when the booking carries no price information
*/
public function createDownloadResponse(AccommodationBooking $booking): Response
{
$response = new Response($this->render($booking));
$response->headers->set('Content-Type', 'application/pdf');
$response->headers->set('Content-Disposition', $response->headers->makeDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
$this->buildFilename($booking)
));
return $response;
}
/**
* Named after arrival date and group rather than the entity id, which is a local
* detail with no counterpart in BusPro and would read as a booking number.
*/
/**
* @throws \RuntimeException when the booking carries no price information
*/
public function createAttachment(AccommodationBooking $booking): PdfAttachment
{
return new PdfAttachment($this->render($booking), $this->buildFilename($booking));
}
private function buildFilename(AccommodationBooking $booking): string
{
$groupName = (new AsciiSlugger('de'))->slug((string) $booking->getGroupName())->lower()->toString();
return sprintf(
'%s-%s%s.pdf',
mb_strtolower($booking->recordLabel()),
$booking->getDateFrom()?->format('Y-m-d') ?? 'ohne-datum',
'' !== $groupName ? '-'.$groupName : ''
);
}
private function logoDataUri(): string
{
$logo = $this->projectDir.self::LOGO_PATH;
if (false === is_readable($logo)) {
throw new \RuntimeException('Das Logo konnte nicht gelesen werden: '.self::LOGO_PATH);
}
return 'data:image/png;base64,'.base64_encode((string) file_get_contents($logo));
}
}