Files
myep/src/Controller/Voucher/Step1Controller.php
T

220 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Controller\Voucher;
use App\BusProNet\Client\VoucherClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\Exception\VoucherSessionNotFoundException;
use App\Exception\VoucherShopUnavailableException;
use App\Form\Model\VoucherPurchaseDto;
use App\Form\VoucherPurchaseStep1Type;
use App\Service\VoucherPurchaseSessionManager;
use App\Service\VoucherTemplateProvider;
use App\Service\VoucherThumbnailResolver;
use Psr\Log\LoggerInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* Step 1: design, von/für and value, with a PDF preview rendered by BusPro.
*/
class Step1Controller extends AbstractVoucherPurchaseController
{
public function __construct(
VoucherPurchaseSessionManager $sessionManager,
VoucherThumbnailResolver $thumbnailResolver,
private readonly VoucherTemplateProvider $templateProvider,
private readonly VoucherClient $voucherClient,
private readonly ValidatorInterface $validator,
private readonly LoggerInterface $logger,
) {
parent::__construct($sessionManager, $thumbnailResolver);
}
#[Route('/vouchers/create/design', name: 'app_voucher_step_1')]
public function index(Request $request): Response
{
$dto = $this->sessionManager->find($request);
if (null === $dto) {
return $this->createSessionExpiredResponse($request);
}
try {
$form = $this->createStepForm($dto);
} catch (VoucherShopUnavailableException) {
return $this->createShopUnavailableResponse($request);
}
$form->handleRequest($request);
if (true === $form->isSubmitted() && true === $form->isValid()) {
$dto->currentStep = 2;
$this->sessionManager->save($request, $dto);
return $this->redirectToRoute('app_voucher_step_2');
}
return $this->renderStep($dto, $form);
}
/**
* Takes over the current input without validating it and refreshes the summary.
*/
#[Route('/vouchers/create/refresh', name: 'app_voucher_step_1_refresh', methods: ['POST'])]
public function refresh(Request $request): Response
{
$dto = $this->sessionManager->find($request);
if (null === $dto) {
return $this->createSessionExpiredResponse($request);
}
try {
$form = $this->createStepForm($dto, false);
} catch (VoucherShopUnavailableException) {
return $this->createShopUnavailableResponse($request);
}
$form->handleRequest($request);
$this->sessionManager->save($request, $dto);
return $this->htmxOobResponse('voucher/step_1.html.twig', ['voucher_summary'], [
...$this->createStepContext($dto),
'form' => $form->createView(),
]);
}
/**
* Takes over the current input like refresh() and opens the preview modal, whose frame loads
* the PDF from previewPdf().
*/
#[Route('/vouchers/create/preview', name: 'app_voucher_preview', methods: ['POST'])]
public function preview(Request $request): Response
{
$dto = $this->sessionManager->find($request);
if (null === $dto) {
return $this->createSessionExpiredResponse($request);
}
try {
$form = $this->createStepForm($dto, false);
} catch (VoucherShopUnavailableException) {
return $this->createShopUnavailableResponse($request);
}
$form->handleRequest($request);
$this->sessionManager->save($request, $dto);
return $this->render('voucher/modal_preview.html.twig', [
'previewable' => $this->isPreviewable($dto),
// busts the browser cache of the frame, the URL is otherwise the same for every input
'previewVersion' => bin2hex(random_bytes(4)),
]);
}
#[Route('/vouchers/create/preview.pdf', name: 'app_voucher_preview_pdf', methods: ['GET'])]
public function previewPdf(Request $request): Response
{
try {
$dto = $this->sessionManager->get($request);
} catch (VoucherSessionNotFoundException) {
return $this->createPreviewErrorResponse('Deine Sitzung ist abgelaufen. Bitte starte den Gutscheinkauf neu.');
}
if (false === $this->isPreviewable($dto)) {
return $this->createPreviewErrorResponse('Für die Vorschau bitte zuerst Motiv, Von, Für und Gutscheinwert angeben.');
}
// Read-only from here on: release the session lock, so the page's other requests do not
// wait the seconds BusPro takes to render the PDF.
$request->getSession()->save();
try {
$preview = $this->voucherClient->getVoucherPreview($dto);
} catch (ApiClientException $e) {
$this->logger->error('Voucher preview failed', ['error' => $e->getMessage()]);
return $this->createPreviewErrorResponse('Die Vorschau ist gerade nicht verfügbar. Bitte versuche es später noch einmal.');
}
if ($preview instanceof Notification) {
$this->logger->warning('Voucher preview rejected', ['code' => $preview->code, 'message' => $preview->message]);
return $this->createPreviewErrorResponse($preview->message ?? 'Die Vorschau ist gerade nicht verfügbar.');
}
$response = new Response($preview->pdf, Response::HTTP_OK, ['Content-Type' => 'application/pdf']);
$response->headers->set('Content-Disposition', $response->headers->makeDisposition(
ResponseHeaderBag::DISPOSITION_INLINE,
'gutschein-vorschau.pdf'
));
$response->setPrivate();
$response->headers->addCacheControlDirective('no-store');
return $response;
}
/**
* @return FormInterface<VoucherPurchaseDto>
*
* @throws VoucherShopUnavailableException
*/
private function createStepForm(VoucherPurchaseDto $dto, bool $validate = true): FormInterface
{
$templates = $this->templateProvider->getTemplates();
$options = [
'templates' => $templates->templates,
'min_amount' => $dto->minAmount,
'max_amount' => $dto->maxAmount,
];
if (false === $validate) {
$options['validation_groups'] = false;
}
return $this->createForm(VoucherPurchaseStep1Type::class, $dto, $options);
}
/**
* @param FormInterface<VoucherPurchaseDto> $form
*/
private function renderStep(VoucherPurchaseDto $dto, FormInterface $form): Response
{
$templates = $this->templateProvider->getTemplates();
return $this->render('voucher/step_1.html.twig', [
...$this->createStepContext($dto),
'form' => $form,
'thumbnails' => $this->thumbnailResolver->resolveAll($templates->templates),
'previewable' => $this->isPreviewable($dto),
]);
}
/**
* The preview takes what step 1 accepts: BusPro refuses it without the names (604/605), and
* this keeps the button and the route to one rule, the step's own validation group.
*/
private function isPreviewable(VoucherPurchaseDto $dto): bool
{
return 0 === \count($this->validator->validate($dto, null, ['voucher_step_1']));
}
private function createShopUnavailableResponse(Request $request): Response
{
$this->addFlash('error', 'Gutscheine können gerade nicht gekauft werden. Bitte versuche es später noch einmal.');
return $this->htmxRedirect($request, $this->generateUrl('app_voucher_error'));
}
private function createPreviewErrorResponse(string $message): Response
{
return $this->render('voucher/preview_error.html.twig', ['message' => $message], new Response('', Response::HTTP_UNPROCESSABLE_ENTITY));
}
}