119 lines
4.0 KiB
PHP
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));
|
|
}
|
|
}
|