feat: extract booking session handling

This commit is contained in:
Björn Fromme
2026-04-11 18:28:32 +02:00
parent ca052682bb
commit 8b72f8c277
21 changed files with 485 additions and 330 deletions
@@ -12,6 +12,7 @@ use App\Exception\TravelNotFoundException;
use App\Htmx\HxTrait;
use App\Model\BookingQueryParams;
use App\Service\BookingService;
use App\Service\BookingSessionService;
use App\Service\BookingSummaryDataService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@@ -32,6 +33,7 @@ class IndexController extends AbstractController
public function __construct(
private readonly BookingService $bookingService,
private readonly BookingSessionService $bookingSessionService,
private readonly AgencyLoader $agencyLoader,
private readonly BookingSummaryDataService $summaryDataService,
) {
@@ -84,13 +86,13 @@ class IndexController extends AbstractController
try {
// Clear any existing booking session to ensure fresh start
$this->bookingService->clearBookingSession($request);
$this->bookingSessionService->clearBookingSession($request);
// Determine agency ID from optional query parameter
$agencyId = $this->resolveAgencyId($params->agency);
// Store optional return URL in session (defaults to main EP site)
$this->bookingService->storeReturnUrl($request, $params->returnUrl);
$this->bookingSessionService->storeReturnUrl($request, $params->returnUrl);
// Create fresh booking session with the provided parameters
$bookingDto = $this->bookingService->startFreshBooking($request, $dateId, $hotelId, $agencyId);
@@ -154,10 +156,10 @@ class IndexController extends AbstractController
{
if (Request::METHOD_POST === $request->getMethod()) {
// Get return URL before clearing session (for guest users)
$returnUrl = $this->bookingService->getReturnUrl($request);
$returnUrl = $this->bookingSessionService->getReturnUrl($request);
// Clear the booking session
$this->bookingService->clearBookingSession($request);
$this->bookingSessionService->clearBookingSession($request);
// Clear the security target path to prevent redirect loop after login
// Without this, logging in after cancel would redirect back to a stale booking URL
@@ -188,7 +190,7 @@ class IndexController extends AbstractController
public function error(Request $request): Response
{
return $this->render('booking/create/error.html.twig', [
'returnUrl' => $this->bookingService->getReturnUrl($request),
'returnUrl' => $this->bookingSessionService->getReturnUrl($request),
]);
}
}
@@ -10,6 +10,7 @@ use App\Form\BookingCreateStep1Type;
use App\Form\Model\BookingDto;
use App\Htmx\HxTrait;
use App\Service\BookingService;
use App\Service\BookingSessionService;
use App\Service\BookingSummaryDataService;
use App\Service\RoomPricingCalculator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -31,6 +32,7 @@ class Step1Controller extends AbstractController
public function __construct(
private readonly BookingService $bookingService,
private readonly BookingSessionService $bookingSessionService,
private readonly BookingSummaryDataService $summaryDataService,
) {
}
@@ -41,14 +43,14 @@ class Step1Controller extends AbstractController
#[Route('/bookings/create/rooms', name: 'app_booking_create_step_1')]
public function index(Request $request): Response
{
$result = $this->getOrCreateBookingCreateDto($this->bookingService, $request);
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
if ($result instanceof Response) {
return $result;
}
$bookingCreateDto = $result;
// Get or create baseline snapshot for change detection
$oldRoomSelectionSnapshot = $this->bookingService->getOrCreateBaselineSnapshot($request, $bookingCreateDto);
$oldRoomSelectionSnapshot = $this->bookingSessionService->getOrCreateBaselineSnapshot($request, $bookingCreateDto);
// Validate step access - allow step 1 or redirect to current step
if ($redirect = $this->validateStepAccess($bookingCreateDto, 1)) {
@@ -70,10 +72,10 @@ class Step1Controller extends AbstractController
$this->bookingService->updateBookingStatusFromRoomSelection($bookingCreateDto);
$bookingCreateDto->currentStep = 2;
$this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
$this->bookingSessionService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
// Clear baseline snapshot when moving to step 2
$this->bookingService->clearBaselineSnapshot($request);
$this->bookingSessionService->clearBaselineSnapshot($request);
return $this->redirectToRoute('app_booking_create_step_2');
}
@@ -9,6 +9,7 @@ use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
use App\Form\BookingCreateStep2Type;
use App\Form\Model\BookingDto;
use App\Service\BookingService;
use App\Service\BookingSessionService;
use App\Service\BookingSummaryDataService;
use App\Service\ParticipantCardDataService;
use App\Service\ParticipantPrepopulationService;
@@ -32,6 +33,7 @@ class Step2Controller extends AbstractController
public function __construct(
private readonly BookingService $bookingService,
private readonly BookingSessionService $bookingSessionService,
private readonly BookingSummaryDataService $summaryDataService,
private readonly TravelDataService $travelDataService,
private readonly RoomAssignmentService $roomAssignmentService,
@@ -47,7 +49,7 @@ class Step2Controller extends AbstractController
public function index(Request $request): Response
{
// Load or create booking DTO
$result = $this->getOrCreateBookingCreateDto($this->bookingService, $request);
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
if ($result instanceof Response) {
return $result;
}
@@ -79,7 +81,7 @@ class Step2Controller extends AbstractController
$this->bookingService->applyCreateBookingStatusRules($bookingCreateDto);
// Save BookingDto to session
$this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
$this->bookingSessionService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
// Create validation form
$form = $this->createForm(BookingCreateStep2Type::class, $bookingCreateDto);
@@ -89,7 +91,7 @@ class Step2Controller extends AbstractController
if (true === $form->isSubmitted() && true === $form->isValid()) {
// All participants validated successfully, update current step
$bookingCreateDto->currentStep = 3;
$this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
$this->bookingSessionService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
// Proceed to Step 3
return $this->redirectToRoute('app_booking_create_step_3');
@@ -9,6 +9,7 @@ use App\Form\Model\BookingDto;
use App\Form\Service\DummyDataFillService;
use App\Htmx\HxTrait;
use App\Service\BookingService;
use App\Service\BookingSessionService;
use App\Service\BookingSummaryDataService;
use App\Service\ParticipantFormSupportService;
use App\Service\TravelDataService;
@@ -27,6 +28,7 @@ class Step2ParticipantController extends AbstractController
public function __construct(
private readonly BookingService $bookingService,
private readonly BookingSessionService $bookingSessionService,
private readonly BookingSummaryDataService $summaryDataService,
private readonly TravelDataService $travelDataService,
private readonly DummyDataFillService $dummyDataFillService,
@@ -68,7 +70,7 @@ class Step2ParticipantController extends AbstractController
$this->bookingService->preselectDefaultServices($bookingDto);
$this->bookingService->applyCreateBookingStatusRules($bookingDto);
$this->bookingService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE);
$this->bookingSessionService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE);
$form = $this->createParticipantForm($bookingDto, $index);
@@ -83,7 +85,7 @@ class Step2ParticipantController extends AbstractController
$notifications = $this->participantFormSupportService->collectAndClearNotifications($bookingDto);
if (true === $isSubmitted && true === $form->isValid()) {
$this->bookingService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE);
$this->bookingSessionService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE);
$this->addNotificationsAsFlashMessages($notifications);
return $this->redirectToRoute('app_booking_create_step_2');
@@ -137,7 +139,7 @@ class Step2ParticipantController extends AbstractController
private function loadBookingDtoOrRedirect(Request $request): BookingDto|Response
{
$bookingDto = $this->bookingService->getBookingDto($request, BookingDto::MODE_CREATE);
$bookingDto = $this->bookingSessionService->getBookingDto($request, BookingDto::MODE_CREATE);
if (null === $bookingDto) {
$this->addFlash('info', 'Deine Sitzung ist abgelaufen. Bitte starte eine neue Buchung.');
@@ -186,7 +188,7 @@ class Step2ParticipantController extends AbstractController
$notifications = $this->participantFormSupportService->collectAndClearNotifications($bookingDto);
$this->bookingService->saveBookingDto($request, $bookingDto, $bookingDto->getMode());
$this->bookingSessionService->saveBookingDto($request, $bookingDto, $bookingDto->getMode());
$summaryData = $this->summaryDataService->getSummaryData($bookingDto);
@@ -16,6 +16,7 @@ use App\Htmx\HxTrait;
use App\Service\BookingPriceCalculatorService;
use App\Service\BookingPriceMismatchDiagnosticsService;
use App\Service\BookingService;
use App\Service\BookingSessionService;
use App\Service\BookingSummaryDataService;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -35,6 +36,7 @@ class Step3Controller extends AbstractController
public function __construct(
private readonly BookingService $bookingService,
private readonly BookingSessionService $bookingSessionService,
private readonly BookingSummaryDataService $summaryDataService,
private readonly BookingPriceCalculatorService $priceCalculator,
private readonly BookingPriceMismatchDiagnosticsService $priceMismatchDiagnostics,
@@ -49,7 +51,7 @@ class Step3Controller extends AbstractController
#[Route('/bookings/create/payment', name: 'app_booking_create_step_3')]
public function index(Request $request): Response
{
$result = $this->getOrCreateBookingCreateDto($this->bookingService, $request);
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
if ($result instanceof Response) {
return $result;
}
@@ -91,7 +93,7 @@ class Step3Controller extends AbstractController
// Auto-switch to inquiry mode
$bookingCreateDto->bookingStatus = 'A';
$bookingCreateDto->currentStep = 4;
$this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
$this->bookingSessionService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
$message = 'Buchung konnte nicht validiert werden.';
if (null !== $inquiryResponse->message && '' !== trim($inquiryResponse->message)) {
@@ -156,7 +158,7 @@ class Step3Controller extends AbstractController
// Validation successful - proceed to confirmation step
$bookingCreateDto->currentStep = 4;
$this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
$this->bookingSessionService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
if ($inquiryResponse->message) {
$this->addFlash('info', $inquiryResponse->message);
@@ -195,7 +197,7 @@ class Step3Controller extends AbstractController
#[Route('/bookings/create/payment/refresh', name: 'app_booking_create_step_3_refresh', methods: ['POST'])]
public function refresh(Request $request): Response
{
$result = $this->getOrCreateBookingCreateDto($this->bookingService, $request);
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
if ($result instanceof Response) {
return $result;
}
@@ -16,6 +16,7 @@ use App\Htmx\HxTrait;
use App\Entity\User;
use App\Service\BookingPriceCalculatorService;
use App\Service\BookingService;
use App\Service\BookingSessionService;
use App\Service\BookingSummaryDataService;
use App\Service\Newsletter\MailjetNewsletterService;
use App\Service\Newsletter\NewsletterDoubleOptInService;
@@ -38,6 +39,7 @@ class Step4Controller extends AbstractController
public function __construct(
private readonly BookingService $bookingService,
private readonly BookingSessionService $bookingSessionService,
private readonly BookingSummaryDataService $summaryDataService,
private readonly BookingPriceCalculatorService $priceCalculator,
private readonly ApiClient $apiClient,
@@ -54,7 +56,7 @@ class Step4Controller extends AbstractController
#[Route('/bookings/create/confirmation', name: 'app_booking_create_step_4')]
public function index(Request $request): Response
{
$result = $this->getOrCreateBookingCreateDto($this->bookingService, $request);
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
if ($result instanceof Response) {
return $result;
}
@@ -137,7 +139,7 @@ class Step4Controller extends AbstractController
$this->addFlash('booking_total', $summaryData->payableAmount);
$this->addFlash('booking_travel_name', $bookingCreateDto->travel->label);
$this->clearTravelDataCache($bookingCreateDto);
$this->bookingService->clearBookingDto($request, BookingDto::MODE_CREATE);
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_CREATE);
$this->logger->info('Booking successfully created.', [
'date_id' => $bookingCreateDto->travel->id,
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Booking\Create;
use App\Service\BookingService;
use App\Service\BookingSessionService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -16,7 +16,7 @@ use Symfony\Component\Routing\Attribute\Route;
class SuccessController extends AbstractController
{
public function __construct(
private readonly BookingService $bookingService,
private readonly BookingSessionService $bookingSessionService,
) {
}
@@ -27,7 +27,7 @@ class SuccessController extends AbstractController
$bookingNumber = $flashBag->get('booking_number')[0] ?? null;
$bookingTotal = $flashBag->get('booking_total')[0] ?? null;
$travelName = $flashBag->get('booking_travel_name')[0] ?? null;
$returnUrl = $this->bookingService->getReturnUrl($request);
$returnUrl = $this->bookingSessionService->getReturnUrl($request);
// Redirect to return URL if no booking number (direct access or refresh)
if (null === $bookingNumber) {
@@ -17,8 +17,8 @@ use App\Htmx\HxTrait;
use App\Service\BookingEditDataLoaderService;
use App\Service\BookingEditDraftService;
use App\Service\BookingFingerprintService;
use App\Service\BookingService;
use App\Service\BookingEditSubmitGuardService;
use App\Service\BookingSessionService;
use App\Service\BookingSummaryDataService;
use App\Service\ParticipantCardDataService;
use App\Service\TravelDataService;
@@ -48,7 +48,7 @@ class IndexController extends AbstractController
private readonly BookingEditDataLoaderService $dataLoader,
private readonly BookingEditDraftService $draftService,
private readonly TravelDataService $travelDataService,
private readonly BookingService $bookingService,
private readonly BookingSessionService $bookingSessionService,
private readonly BookingEditSubmitGuardService $submitGuard,
private readonly BookingFingerprintService $fingerprintService,
private readonly BookingSummaryDataService $summaryDataService,
@@ -70,7 +70,7 @@ class IndexController extends AbstractController
/** @var User $user */
$user = $this->getUser();
$this->bookingService->clearBookingDto($request, BookingDto::MODE_EDIT);
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_EDIT);
$this->dataLoader->invalidateBookingCache($id, $user);
return $this->redirectToRoute('app_booking_edit', ['id' => $id]);
@@ -163,7 +163,7 @@ class IndexController extends AbstractController
{
if (Request::METHOD_POST === $request->getMethod()) {
// Clear session to discard all changes
$this->bookingService->clearBookingDto($request, BookingDto::MODE_EDIT);
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_EDIT);
// Delete draft since user explicitly chose to discard changes
/** @var User $user */
@@ -195,7 +195,7 @@ class IndexController extends AbstractController
public function cancelEdit(int $id, Request $request): Response
{
// Clear session state
$this->bookingService->clearBookingDto($request, BookingDto::MODE_EDIT);
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_EDIT);
// Check if a draft exists to show appropriate message
/** @var User $user */
@@ -246,7 +246,7 @@ class IndexController extends AbstractController
$immutableChangesReverted = $this->submitGuard->reconcileImmutableCategories($bookingDto, $freshBookingData);
if (true === $immutableChangesReverted) {
$this->bookingService->saveBookingDto($request, $bookingDto, BookingDto::MODE_EDIT);
$this->bookingSessionService->saveBookingDto($request, $bookingDto, BookingDto::MODE_EDIT);
$this->addFlash('info', 'Einige Änderungen wurden verworfen, da sie aktuell nicht mehr änderbar sind.');
}
@@ -266,7 +266,7 @@ class IndexController extends AbstractController
} elseif (true === $response->success) {
// Invalidate cache and clear session on success
$this->dataLoader->invalidateBookingCache($id, $user);
$this->bookingService->clearBookingDto($request, BookingDto::MODE_EDIT);
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_EDIT);
// Delete draft on successful submission
$this->draftService->deleteDraft($user, $id);
@@ -9,6 +9,7 @@ use App\Entity\User;
use App\Form\BookingParticipantType;
use App\Htmx\HxTrait;
use App\Service\BookingEditParticipantFormService;
use App\Service\BookingSessionService;
use App\Service\ParticipantFormSupportService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@@ -26,6 +27,7 @@ class ParticipantController extends AbstractController
public function __construct(
private readonly ParticipantFormSupportService $participantFormSupportService,
private readonly BookingEditParticipantFormService $participantFormService,
private readonly BookingSessionService $bookingSessionService,
) {
}
@@ -10,7 +10,7 @@ use App\Exception\HotelNotInTravelException;
use App\Exception\NoRoomsAvailableException;
use App\Exception\TravelNotFoundException;
use App\Form\Model\BookingDto;
use App\Service\BookingService;
use App\Service\BookingSessionService;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -29,10 +29,10 @@ trait BookingExceptionHandlerTrait
* Handles all booking-related exceptions and provides appropriate user feedback
* by redirecting to the error page with flash messages.
*/
protected function getOrCreateBookingCreateDto(BookingService $bookingService, Request $request): BookingDto|RedirectResponse
protected function getOrCreateBookingCreateDto(BookingSessionService $bookingSessionService, Request $request): BookingDto|RedirectResponse
{
try {
return $bookingService->getOrCreateBookingCreateDto($request);
return $bookingSessionService->getOrCreateBookingCreateDto($request);
} catch (BookingSessionNotFoundException $e) {
$this->addFlash('error', 'Deine Buchungssitzung ist abgelaufen. Bitte starte eine neue Buchung.');
@@ -62,10 +62,10 @@ trait BookingExceptionHandlerTrait
* Returns empty 400 responses for HTMX requests when exceptions occur,
* allowing the frontend to handle errors appropriately.
*/
protected function getOrCreateBookingCreateDtoForHtmx(BookingService $bookingService, Request $request): mixed
protected function getOrCreateBookingCreateDtoForHtmx(BookingSessionService $bookingSessionService, Request $request): mixed
{
try {
return $bookingService->getOrCreateBookingCreateDto($request);
return $bookingSessionService->getOrCreateBookingCreateDto($request);
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotFoundException|HotelNotInTravelException|NoRoomsAvailableException $e) {
return new Response('', 400);
}
+3 -3
View File
@@ -2,7 +2,7 @@
namespace App\Controller;
use App\Service\BookingService;
use App\Service\BookingSessionService;
use App\Service\BookingSummaryDataService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@@ -18,11 +18,11 @@ class SecurityController extends AbstractController
public function login(
AuthenticationUtils $authenticationUtils,
Request $request,
BookingService $bookingService,
BookingSessionService $bookingSessionService,
BookingSummaryDataService $summaryDataService,
): Response {
// Check if this is a booking flow (BookingDto exists in session)
$bookingDto = $bookingService->getBookingDto($request, BookingService::BOOKING_CREATE_KEY);
$bookingDto = $bookingSessionService->getBookingDto($request, BookingSessionService::BOOKING_CREATE_KEY);
$isBookingFlow = null !== $bookingDto;
// If authenticated and in booking flow, proceed to Step 1