feat: improved error handling in controllers, cleanup
This commit is contained in:
@@ -314,7 +314,7 @@ class TravelLoader extends AbstractLoader
|
|||||||
$travelData = $this->loadById($travelId);
|
$travelData = $this->loadById($travelId);
|
||||||
$booking->travelData = $travelData;
|
$booking->travelData = $travelData;
|
||||||
|
|
||||||
if (null === $travelData || null === $travelData->dateFrom) {
|
if (null === $travelData->dateFrom) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,16 +17,20 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
|
|||||||
trait BookingCreateTrait
|
trait BookingCreateTrait
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Validates step access and redirects if necessary.
|
* Validates step access and returns redirect response if necessary.
|
||||||
|
*
|
||||||
|
* @return RedirectResponse|null Returns redirect response if validation fails, null if access is allowed
|
||||||
*/
|
*/
|
||||||
private function validateStepAccess(BookingCreateDto $bookingCreateDto, int $expectedStep): void
|
private function validateStepAccess(BookingCreateDto $bookingCreateDto, int $expectedStep): ?RedirectResponse
|
||||||
{
|
{
|
||||||
// Allow access to current step or any previous step
|
// Allow access to current step or any previous step
|
||||||
if ($expectedStep > $bookingCreateDto->currentStep) {
|
if ($expectedStep > $bookingCreateDto->currentStep) {
|
||||||
$this->addFlash('error', 'Bitte erst die vorherigen Schritte abschließen.');
|
$this->addFlash('error', 'Bitte erst die vorherigen Schritte abschließen.');
|
||||||
|
|
||||||
$this->redirectToCurrentStep($bookingCreateDto);
|
return $this->redirectToCurrentStep($bookingCreateDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,18 +38,12 @@ trait BookingCreateTrait
|
|||||||
*/
|
*/
|
||||||
private function redirectToCurrentStep(BookingCreateDto $bookingCreateDto): RedirectResponse
|
private function redirectToCurrentStep(BookingCreateDto $bookingCreateDto): RedirectResponse
|
||||||
{
|
{
|
||||||
$routeParams = [
|
|
||||||
'date_id' => $bookingCreateDto->travel->id,
|
|
||||||
'hotel_id' => $bookingCreateDto->travel->hotelId,
|
|
||||||
];
|
|
||||||
|
|
||||||
$route = match ($bookingCreateDto->currentStep) {
|
$route = match ($bookingCreateDto->currentStep) {
|
||||||
1 => 'app_booking_create_step_1',
|
|
||||||
2 => 'app_booking_create_step_2',
|
2 => 'app_booking_create_step_2',
|
||||||
3 => 'app_booking_create_step_3',
|
3 => 'app_booking_create_step_3',
|
||||||
default => 'app_booking_create_step_1',
|
default => 'app_booking_create_step_1',
|
||||||
};
|
};
|
||||||
|
|
||||||
return $this->redirectToRoute($route, $routeParams);
|
return $this->redirectToRoute($route);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Booking;
|
||||||
|
|
||||||
|
use App\Exception\BookingSessionNotFoundException;
|
||||||
|
use App\Exception\HotelNotFoundException;
|
||||||
|
use App\Exception\HotelNotInTravelException;
|
||||||
|
use App\Exception\NoRoomsAvailableException;
|
||||||
|
use App\Exception\TravelNotFoundException;
|
||||||
|
use App\Service\BookingService;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides centralized exception handling for booking controllers.
|
||||||
|
*
|
||||||
|
* This trait contains common exception handling logic for booking operations
|
||||||
|
* including user-friendly error messages and appropriate redirects.
|
||||||
|
*/
|
||||||
|
trait BookingExceptionHandlerTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Safely retrieves booking DTO with centralized exception handling.
|
||||||
|
*
|
||||||
|
* 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): mixed
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $bookingService->getOrCreateBookingCreateDto($request);
|
||||||
|
} catch (BookingSessionNotFoundException $e) {
|
||||||
|
$this->addFlash('error', 'Ihre Buchungssitzung ist abgelaufen. Bitte starten Sie eine neue Buchung.');
|
||||||
|
return $this->redirectToRoute('app_booking_create_error');
|
||||||
|
} catch (TravelNotFoundException $e) {
|
||||||
|
$this->addFlash('error', 'Die angeforderte Reise wurde nicht gefunden.');
|
||||||
|
return $this->redirectToRoute('app_booking_create_error');
|
||||||
|
} catch (HotelNotFoundException $e) {
|
||||||
|
$this->addFlash('error', 'Das angeforderte Hotel wurde nicht gefunden.');
|
||||||
|
return $this->redirectToRoute('app_booking_create_error');
|
||||||
|
} catch (HotelNotInTravelException $e) {
|
||||||
|
$this->addFlash('error', 'Das Hotel ist für diese Reise nicht verfügbar.');
|
||||||
|
return $this->redirectToRoute('app_booking_create_error');
|
||||||
|
} catch (NoRoomsAvailableException $e) {
|
||||||
|
$this->addFlash('error', 'Für diese Reise sind aktuell keine Zimmer verfügbar.');
|
||||||
|
return $this->redirectToRoute('app_booking_create_error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely retrieves booking DTO for HTMX requests with lightweight error responses.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $bookingService->getOrCreateBookingCreateDto($request);
|
||||||
|
} catch (BookingSessionNotFoundException | TravelNotFoundException | HotelNotFoundException | HotelNotInTravelException | NoRoomsAvailableException $e) {
|
||||||
|
return new Response('', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,4 +57,16 @@ class CreateInitController extends AbstractController
|
|||||||
throw $this->createNotFoundException('No rooms available for this travel.');
|
throw $this->createNotFoundException('No rooms available for this travel.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays user-friendly error messages for booking initialization failures.
|
||||||
|
*
|
||||||
|
* This endpoint provides a centralized location for displaying booking errors
|
||||||
|
* with appropriate error messages and guidance for users.
|
||||||
|
*/
|
||||||
|
#[Route('/bookings/create/error', name: 'app_booking_create_error')]
|
||||||
|
public function error(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->render('booking/create_error.html.twig');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Booking;
|
namespace App\Controller\Booking;
|
||||||
|
|
||||||
use App\Exception\NoRoomsAvailableException;
|
|
||||||
use App\Form\BookingCreateStep1Type;
|
use App\Form\BookingCreateStep1Type;
|
||||||
use App\Service\BookingService;
|
use App\Service\BookingService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -21,6 +20,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
class CreateStep1Controller extends AbstractController
|
class CreateStep1Controller extends AbstractController
|
||||||
{
|
{
|
||||||
use BookingCreateTrait;
|
use BookingCreateTrait;
|
||||||
|
use BookingExceptionHandlerTrait;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly BookingService $bookingService,
|
private readonly BookingService $bookingService,
|
||||||
@@ -33,11 +33,11 @@ class CreateStep1Controller extends AbstractController
|
|||||||
#[Route('/bookings/create', name: 'app_booking_create_step_1')]
|
#[Route('/bookings/create', name: 'app_booking_create_step_1')]
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
try {
|
$result = $this->getOrCreateBookingCreateDto($this->bookingService, $request);
|
||||||
$bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request);
|
if ($result instanceof Response) {
|
||||||
} catch (NoRoomsAvailableException $e) {
|
return $result;
|
||||||
throw $this->createNotFoundException('No rooms available for this travel.');
|
|
||||||
}
|
}
|
||||||
|
$bookingCreateDto = $result;
|
||||||
|
|
||||||
// Get or create baseline snapshot for change detection
|
// Get or create baseline snapshot for change detection
|
||||||
$oldRoomSelectionSnapshot = $this->bookingService->getOrCreateBaselineSnapshot($request, $bookingCreateDto);
|
$oldRoomSelectionSnapshot = $this->bookingService->getOrCreateBaselineSnapshot($request, $bookingCreateDto);
|
||||||
@@ -45,7 +45,9 @@ class CreateStep1Controller extends AbstractController
|
|||||||
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
|
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
|
||||||
|
|
||||||
// Validate step access - allow step 1 or redirect to current step
|
// Validate step access - allow step 1 or redirect to current step
|
||||||
$this->validateStepAccess($bookingCreateDto, 1);
|
if ($redirect = $this->validateStepAccess($bookingCreateDto, 1)) {
|
||||||
|
return $redirect;
|
||||||
|
}
|
||||||
|
|
||||||
$form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto, [
|
$form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto, [
|
||||||
'validation_groups' => ['booking_create_step_1'],
|
'validation_groups' => ['booking_create_step_1'],
|
||||||
@@ -88,11 +90,11 @@ class CreateStep1Controller extends AbstractController
|
|||||||
#[Route('/bookings/create/room-summary', name: 'app_booking_create_step_1_room_summary', methods: ['POST'])]
|
#[Route('/bookings/create/room-summary', name: 'app_booking_create_step_1_room_summary', methods: ['POST'])]
|
||||||
public function roomSummary(Request $request): Response
|
public function roomSummary(Request $request): Response
|
||||||
{
|
{
|
||||||
try {
|
$result = $this->getOrCreateBookingCreateDto($this->bookingService, $request);
|
||||||
$bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request);
|
if ($result instanceof Response) {
|
||||||
} catch (NoRoomsAvailableException $e) {
|
return $result;
|
||||||
throw $this->createNotFoundException('No rooms available for this travel.');
|
|
||||||
}
|
}
|
||||||
|
$bookingCreateDto = $result;
|
||||||
|
|
||||||
// Process the form to update the DTO with the latest room selection
|
// Process the form to update the DTO with the latest room selection
|
||||||
$form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto, [
|
$form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto, [
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\Controller\Booking;
|
namespace App\Controller\Booking;
|
||||||
|
|
||||||
use App\Controller\Traits\HtmxControllerTrait;
|
use App\Controller\Traits\HtmxControllerTrait;
|
||||||
use App\Exception\NoRoomsAvailableException;
|
|
||||||
use App\Form\BookingCreateStep2Type;
|
use App\Form\BookingCreateStep2Type;
|
||||||
use App\Form\Model\BookingCreateDto;
|
use App\Form\Model\BookingCreateDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
@@ -25,6 +24,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
class CreateStep2Controller extends AbstractController
|
class CreateStep2Controller extends AbstractController
|
||||||
{
|
{
|
||||||
use BookingCreateTrait;
|
use BookingCreateTrait;
|
||||||
|
use BookingExceptionHandlerTrait;
|
||||||
use HtmxControllerTrait;
|
use HtmxControllerTrait;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -39,21 +39,19 @@ class CreateStep2Controller extends AbstractController
|
|||||||
#[Route('/bookings/create/participants', name: 'app_booking_create_step_2')]
|
#[Route('/bookings/create/participants', name: 'app_booking_create_step_2')]
|
||||||
public function participants(Request $request): Response
|
public function participants(Request $request): Response
|
||||||
{
|
{
|
||||||
try {
|
$result = $this->getOrCreateBookingCreateDto($this->bookingService, $request);
|
||||||
$bookingCreateDto = $this
|
if ($result instanceof Response) {
|
||||||
->bookingService
|
return $result;
|
||||||
->getOrCreateBookingCreateDto($request);
|
|
||||||
} catch (NoRoomsAvailableException $e) {
|
|
||||||
$this->addFlash('error', 'Leider sind für diese Reise aktuell keine Zimmer verfügbar.');
|
|
||||||
// TODO: Redirect to travel listing or hotel details page
|
|
||||||
throw $this->createNotFoundException('No rooms available for this travel.');
|
|
||||||
}
|
}
|
||||||
|
$bookingCreateDto = $result;
|
||||||
|
|
||||||
// Enrich with fresh availability data
|
// Enrich with fresh availability data
|
||||||
$this->enrichWithFreshAvailabilities($bookingCreateDto);
|
$this->enrichWithFreshAvailabilities($bookingCreateDto);
|
||||||
|
|
||||||
// Validate step access
|
// Validate step access
|
||||||
$this->validateStepAccess($bookingCreateDto, 2);
|
if ($redirect = $this->validateStepAccess($bookingCreateDto, 2)) {
|
||||||
|
return $redirect;
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure correct number of participants
|
// Ensure correct number of participants
|
||||||
$this->ensureCorrectNumberOfParticipants($bookingCreateDto);
|
$this->ensureCorrectNumberOfParticipants($bookingCreateDto);
|
||||||
@@ -100,12 +98,11 @@ class CreateStep2Controller extends AbstractController
|
|||||||
#[Route('/bookings/create/participants/refresh', name: 'app_booking_create_step_2_refresh', methods: ['POST'])]
|
#[Route('/bookings/create/participants/refresh', name: 'app_booking_create_step_2_refresh', methods: ['POST'])]
|
||||||
public function refreshParticipantForm(Request $request): Response
|
public function refreshParticipantForm(Request $request): Response
|
||||||
{
|
{
|
||||||
try {
|
$result = $this->getOrCreateBookingCreateDtoForHtmx($this->bookingService, $request);
|
||||||
$bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request);
|
if ($result instanceof Response) {
|
||||||
} catch (NoRoomsAvailableException $e) {
|
return $result;
|
||||||
// For HTMX requests, return a simple error message
|
|
||||||
return new Response('<div class="text-red-700 p-4">Keine Zimmer verfügbar</div>', 400);
|
|
||||||
}
|
}
|
||||||
|
$bookingCreateDto = $result;
|
||||||
|
|
||||||
// Enrich with fresh availability data
|
// Enrich with fresh availability data
|
||||||
$this->enrichWithFreshAvailabilities($bookingCreateDto);
|
$this->enrichWithFreshAvailabilities($bookingCreateDto);
|
||||||
@@ -152,6 +149,13 @@ class CreateStep2Controller extends AbstractController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the total number of participants based on room selections.
|
||||||
|
*
|
||||||
|
* @param BookingCreateDto $bookingCreateDto The booking DTO containing room selections
|
||||||
|
*
|
||||||
|
* @return int Total number of participants required
|
||||||
|
*/
|
||||||
private function getParticipantsCount(BookingCreateDto $bookingCreateDto): int
|
private function getParticipantsCount(BookingCreateDto $bookingCreateDto): int
|
||||||
{
|
{
|
||||||
return $this
|
return $this
|
||||||
@@ -159,6 +163,15 @@ class CreateStep2Controller extends AbstractController
|
|||||||
->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travel);
|
->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the booking DTO has the correct number of participant objects.
|
||||||
|
*
|
||||||
|
* Creates or reuses participant DTOs to match the required participant count
|
||||||
|
* based on room selections. Preserves existing participant data when possible
|
||||||
|
* and assigns proper index values.
|
||||||
|
*
|
||||||
|
* @param BookingCreateDto $bookingCreateDto The booking DTO to update
|
||||||
|
*/
|
||||||
private function ensureCorrectNumberOfParticipants(BookingCreateDto $bookingCreateDto): void
|
private function ensureCorrectNumberOfParticipants(BookingCreateDto $bookingCreateDto): void
|
||||||
{
|
{
|
||||||
$participantsCount = $this->getParticipantsCount($bookingCreateDto);
|
$participantsCount = $this->getParticipantsCount($bookingCreateDto);
|
||||||
@@ -177,6 +190,9 @@ class CreateStep2Controller extends AbstractController
|
|||||||
*
|
*
|
||||||
* Fetches availability data with short-term caching and patches the travel object
|
* Fetches availability data with short-term caching and patches the travel object
|
||||||
* to ensure service availability is reasonably up-to-date while reducing API calls.
|
* to ensure service availability is reasonably up-to-date while reducing API calls.
|
||||||
|
* This is essential for accurate pricing and service selection during the booking process.
|
||||||
|
*
|
||||||
|
* @param BookingCreateDto $bookingCreateDto The booking DTO containing travel data to enrich
|
||||||
*/
|
*/
|
||||||
private function enrichWithFreshAvailabilities(BookingCreateDto $bookingCreateDto): void
|
private function enrichWithFreshAvailabilities(BookingCreateDto $bookingCreateDto): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Booking;
|
namespace App\Controller\Booking;
|
||||||
|
|
||||||
use App\Exception\NoRoomsAvailableException;
|
|
||||||
use App\Service\BookingService;
|
use App\Service\BookingService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -20,6 +19,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
class CreateStep3Controller extends AbstractController
|
class CreateStep3Controller extends AbstractController
|
||||||
{
|
{
|
||||||
use BookingCreateTrait;
|
use BookingCreateTrait;
|
||||||
|
use BookingExceptionHandlerTrait;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly BookingService $bookingCreateService,
|
private readonly BookingService $bookingCreateService,
|
||||||
@@ -32,16 +32,16 @@ class CreateStep3Controller extends AbstractController
|
|||||||
#[Route('/bookings/create/confirm', name: 'app_booking_create_step_3')]
|
#[Route('/bookings/create/confirm', name: 'app_booking_create_step_3')]
|
||||||
public function confirm(Request $request): Response
|
public function confirm(Request $request): Response
|
||||||
{
|
{
|
||||||
try {
|
$result = $this->getOrCreateBookingCreateDto($this->bookingCreateService, $request);
|
||||||
$bookingCreateDto = $this->bookingCreateService->getOrCreateBookingCreateDto($request);
|
if ($result instanceof Response) {
|
||||||
} catch (NoRoomsAvailableException $e) {
|
return $result;
|
||||||
$this->addFlash('error', 'Leider sind für diese Reise aktuell keine Zimmer verfügbar.');
|
|
||||||
// TODO: Redirect to travel listing or hotel details page
|
|
||||||
throw $this->createNotFoundException('No rooms available for this travel.');
|
|
||||||
}
|
}
|
||||||
|
$bookingCreateDto = $result;
|
||||||
|
|
||||||
// Validate step access
|
// Validate step access
|
||||||
$this->validateStepAccess($bookingCreateDto, 3);
|
if ($redirect = $this->validateStepAccess($bookingCreateDto, 3)) {
|
||||||
|
return $redirect;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->render('booking/create_step_3.html.twig', [
|
return $this->render('booking/create_step_3.html.twig', [
|
||||||
'bookingCreateDto' => $bookingCreateDto,
|
'bookingCreateDto' => $bookingCreateDto,
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Exception;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when attempting to access booking steps without a valid session.
|
||||||
|
*
|
||||||
|
* This exception is used when step controllers are accessed without going through
|
||||||
|
* the proper initialization flow or when the booking session has expired.
|
||||||
|
*/
|
||||||
|
class BookingSessionNotFoundException extends HttpException
|
||||||
|
{
|
||||||
|
public function __construct(?\Throwable $previous = null)
|
||||||
|
{
|
||||||
|
$message = 'No valid booking session found. Please start a new booking.';
|
||||||
|
|
||||||
|
parent::__construct(404, $message, $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Form\DataTransformer;
|
|
||||||
|
|
||||||
use Symfony\Component\Form\DataTransformerInterface;
|
|
||||||
use voku\helper\AntiXSS;
|
|
||||||
|
|
||||||
class XssCleanTransformer implements DataTransformerInterface
|
|
||||||
{
|
|
||||||
private ?AntiXSS $antiXss = null;
|
|
||||||
|
|
||||||
public function transform(mixed $value): mixed
|
|
||||||
{
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function reverseTransform(mixed $value): mixed
|
|
||||||
{
|
|
||||||
if (false === is_string($value) || true === empty($value)) {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $this->antiXss) {
|
|
||||||
$this->antiXss = new AntiXSS();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->antiXss->xss_clean($value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ namespace App\Service;
|
|||||||
|
|
||||||
use App\BusProNet\Model\Room;
|
use App\BusProNet\Model\Room;
|
||||||
use App\BusProNet\Model\Travel;
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\Exception\BookingSessionNotFoundException;
|
||||||
use App\Exception\NoRoomsAvailableException;
|
use App\Exception\NoRoomsAvailableException;
|
||||||
use App\Form\Model\BookingCreateDto;
|
use App\Form\Model\BookingCreateDto;
|
||||||
use App\Form\Model\RoomSelectionDto;
|
use App\Form\Model\RoomSelectionDto;
|
||||||
@@ -53,6 +54,19 @@ class BookingService
|
|||||||
$request->getSession()->remove('booking_create_baseline_snapshot');
|
$request->getSession()->remove('booking_create_baseline_snapshot');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the booking creation DTO from the session.
|
||||||
|
*
|
||||||
|
* This method enforces the secure booking flow by only returning existing
|
||||||
|
* session data. Users must go through the proper initialization flow via
|
||||||
|
* CreateInitController to create new booking sessions.
|
||||||
|
*
|
||||||
|
* @param Request $request The HTTP request containing session data
|
||||||
|
*
|
||||||
|
* @return BookingCreateDto The booking DTO from session
|
||||||
|
*
|
||||||
|
* @throws BookingSessionNotFoundException When no valid booking session exists
|
||||||
|
*/
|
||||||
public function getOrCreateBookingCreateDto(Request $request): BookingCreateDto
|
public function getOrCreateBookingCreateDto(Request $request): BookingCreateDto
|
||||||
{
|
{
|
||||||
$bookingCreateDto = $request->getSession()->get(self::BOOKING_CREATE_KEY);
|
$bookingCreateDto = $request->getSession()->get(self::BOOKING_CREATE_KEY);
|
||||||
@@ -62,40 +76,19 @@ class BookingService
|
|||||||
return $bookingCreateDto;
|
return $bookingCreateDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy support: Create a new DTO if date_id and hotel_id are provided
|
// No session found - user must go through proper init flow
|
||||||
// This maintains backward compatibility for existing URLs with UID parameters
|
throw new BookingSessionNotFoundException();
|
||||||
$dateId = $request->query->getInt('date_id');
|
|
||||||
$hotelId = $request->query->getInt('hotel_id');
|
|
||||||
|
|
||||||
if (0 === $dateId || 0 === $hotelId) {
|
|
||||||
throw new NotFoundHttpException('No booking session found. Please start a new booking.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$travelData = $this->travelDataService->getTravelData($dateId, $hotelId);
|
|
||||||
if (null === $travelData) {
|
|
||||||
throw new NotFoundHttpException(sprintf('Travel data not found for date ID %d and hotel ID %d', $dateId, $hotelId));
|
|
||||||
}
|
|
||||||
$roomsIdsAndQuantities = $this->processRoomQuantities($request);
|
|
||||||
$availableRooms = $travelData->getAvailableRooms();
|
|
||||||
|
|
||||||
// Prevent booking flow entry when no rooms are available
|
|
||||||
if (empty($availableRooms)) {
|
|
||||||
throw new NoRoomsAvailableException($dateId, $hotelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$roomSelections = array_map(
|
|
||||||
fn (Room $room) => $this->createRoomSelection($room, $roomsIdsAndQuantities),
|
|
||||||
$availableRooms
|
|
||||||
);
|
|
||||||
|
|
||||||
$bookingCreateDto = new BookingCreateDto($travelData, $hotelId);
|
|
||||||
$bookingCreateDto->roomSelections = $roomSelections;
|
|
||||||
|
|
||||||
$this->saveBookingCreateDto($request, $bookingCreateDto);
|
|
||||||
|
|
||||||
return $bookingCreateDto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the booking creation DTO to the session.
|
||||||
|
*
|
||||||
|
* Persists the current booking state to the session for retrieval
|
||||||
|
* across multiple HTTP requests during the booking flow.
|
||||||
|
*
|
||||||
|
* @param Request $request The HTTP request with session
|
||||||
|
* @param BookingCreateDto $bookingCreateDto The booking DTO to persist
|
||||||
|
*/
|
||||||
public function saveBookingCreateDto(Request $request, BookingCreateDto $bookingCreateDto): void
|
public function saveBookingCreateDto(Request $request, BookingCreateDto $bookingCreateDto): void
|
||||||
{
|
{
|
||||||
$request->getSession()->set(self::BOOKING_CREATE_KEY, $bookingCreateDto);
|
$request->getSession()->set(self::BOOKING_CREATE_KEY, $bookingCreateDto);
|
||||||
@@ -150,6 +143,17 @@ class BookingService
|
|||||||
return $bookingCreateDto;
|
return $bookingCreateDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a room selection DTO from room data and quantities.
|
||||||
|
*
|
||||||
|
* Converts a Room model into a RoomSelectionDto with the specified quantity
|
||||||
|
* selection. Used during booking initialization to create selectable room options.
|
||||||
|
*
|
||||||
|
* @param Room $room The room model to convert
|
||||||
|
* @param array $roomsIdsAndQuantities Array of room ID to quantity mappings
|
||||||
|
*
|
||||||
|
* @return RoomSelectionDto The room selection DTO
|
||||||
|
*/
|
||||||
private function createRoomSelection(Room $room, array $roomsIdsAndQuantities): RoomSelectionDto
|
private function createRoomSelection(Room $room, array $roomsIdsAndQuantities): RoomSelectionDto
|
||||||
{
|
{
|
||||||
$selection = new RoomSelectionDto();
|
$selection = new RoomSelectionDto();
|
||||||
@@ -163,23 +167,16 @@ class BookingService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts room IDs and quantities from the request data.
|
* Calculates the total number of participants based on room selections.
|
||||||
*
|
*
|
||||||
* Processes the 'rooms' parameter from the request to extract room IDs
|
* Multiplies each room's minimum occupancy (minPax) by the selected quantity
|
||||||
* as keys and their corresponding quantities as integer values. Filters
|
* to determine the total number of participants required for the booking.
|
||||||
* out empty values and converts all quantities to integers.
|
|
||||||
*
|
*
|
||||||
* @param Request $request The HTTP request containing room data
|
* @param array $roomSelections Array of RoomSelectionDto objects
|
||||||
|
* @param Travel $travelData Travel data containing room information
|
||||||
*
|
*
|
||||||
* @return array<int, int> Array with room IDs as keys and quantities as values
|
* @return int Total number of participants required
|
||||||
*/
|
*/
|
||||||
public function processRoomQuantities(Request $request): array
|
|
||||||
{
|
|
||||||
$rooms = $request->request->all('rooms');
|
|
||||||
|
|
||||||
return array_map('intval', array_filter($rooms, 'strlen'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getParticipantsCount(array $roomSelections, Travel $travelData): int
|
public function getParticipantsCount(array $roomSelections, Travel $travelData): int
|
||||||
{
|
{
|
||||||
$participantsCount = 0;
|
$participantsCount = 0;
|
||||||
@@ -279,6 +276,12 @@ class BookingService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets all participant room assignments in the DTO.
|
* Resets all participant room assignments in the DTO.
|
||||||
|
*
|
||||||
|
* Clears room assignments when room selections change to prevent
|
||||||
|
* invalid assignments. Called when users modify their room selections
|
||||||
|
* in step 1 to ensure participants are reassigned appropriately.
|
||||||
|
*
|
||||||
|
* @param BookingCreateDto $dto The booking DTO to reset assignments for
|
||||||
*/
|
*/
|
||||||
public function resetParticipantAssignments(BookingCreateDto $dto): void
|
public function resetParticipantAssignments(BookingCreateDto $dto): void
|
||||||
{
|
{
|
||||||
@@ -302,6 +305,14 @@ class BookingService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if room selection has changed compared to a previous snapshot.
|
* Checks if room selection has changed compared to a previous snapshot.
|
||||||
|
*
|
||||||
|
* Compares the current room selection state with a baseline snapshot
|
||||||
|
* to detect changes that would require participant reassignment.
|
||||||
|
*
|
||||||
|
* @param array $oldSnapshot The baseline room selection snapshot
|
||||||
|
* @param BookingCreateDto $newDto The current booking DTO
|
||||||
|
*
|
||||||
|
* @return bool True if room selections have changed, false otherwise
|
||||||
*/
|
*/
|
||||||
public function hasRoomSelectionChanged(array $oldSnapshot, BookingCreateDto $newDto): bool
|
public function hasRoomSelectionChanged(array $oldSnapshot, BookingCreateDto $newDto): bool
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{% extends 'layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Booking Error{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="max-w-lg mx-auto">
|
||||||
|
<div class="bg-red-50 border border-red-200 rounded-lg p-6">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-red-800">
|
||||||
|
Booking Error
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2 text-sm text-red-700">
|
||||||
|
{% for flash_message in app.flashes('error') %}
|
||||||
|
<p>{{ flash_message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if app.flashes('error') is empty %}
|
||||||
|
<p>Es ist ein Fehler beim Starten der Buchung aufgetreten. Bitte versuchen Sie es erneut.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<a href="#" class="button bg-button bg-button--secondary">
|
||||||
|
Zur Startseite
|
||||||
|
</a>
|
||||||
|
<button onclick="history.back()" class="button bg-button">
|
||||||
|
Zurück
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user