chore: further cleanup and refactor

This commit is contained in:
Björn Fromme committed 2026-04-19 15:45:02 +02:00
1 parent 5a0bf11c81
commit 80969d1da0
21 files changed
+606 -321

No files matched your search

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace App\Controller\Booking\Create;
use App\Exception\BookingSessionNotFoundException;
use App\Exception\HotelNotInTravelException;
use App\Exception\TravelNotFoundException;
use App\Form\Model\BookingDto;
use App\Service\BookingSessionManager;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Shared base controller for booking creation steps.
*/
abstract class AbstractBookingCreateController extends AbstractController
{
protected function loadBookingCreateDto(BookingSessionManager $bookingSessionService, Request $request): BookingDto
{
return $bookingSessionService->getOrCreateBookingCreateDto($request);
}
protected function createBookingCreateFailureResponse(\Throwable $exception, bool $htmx): Response
{
if (true === $htmx) {
return $this->createBookingCreateHtmxFailureResponse();
}
return $this->createBookingCreateRedirectFailureResponse($exception);
}
protected function validateStepAccess(BookingDto $bookingCreateDto, int $expectedStep): ?RedirectResponse
{
// Allow access to current step or any previous step
if ($expectedStep > $bookingCreateDto->currentStep) {
$this->addFlash('error', 'Bitte erst die vorherigen Schritte abschließen.');
return $this->redirectToCurrentStep($bookingCreateDto);
}
return null;
}
/**
* Handles API errors by logging and adding a flash message.
*
* @param array<string, mixed> $context
*/
protected function handleApiError(
LoggerInterface $logger,
string $logMessage,
array $context,
string $flashMessage,
): void {
$logger->error($logMessage, $context);
$this->addFlash('error', $flashMessage);
}
private function createBookingCreateRedirectFailureResponse(\Throwable $exception): Response
{
$message = match (true) {
$exception instanceof BookingSessionNotFoundException => 'Deine Buchungssitzung ist abgelaufen. Bitte starte eine neue Buchung.',
$exception instanceof TravelNotFoundException => 'Reisedaten sind nicht (mehr) verfügbar.',
$exception instanceof HotelNotInTravelException => 'Das gewählte Hotel ist für diese Reise nicht verfügbar.',
default => 'Die Buchung konnte nicht geladen werden.',
};
$this->addFlash('error', $message);
return $this->redirectToRoute('app_booking_create_error');
}
private function createBookingCreateHtmxFailureResponse(): Response
{
return new Response('', 400);
}
private function redirectToCurrentStep(BookingDto $bookingCreateDto): RedirectResponse
{
$route = match ($bookingCreateDto->currentStep) {
2 => 'app_booking_create_step_2',
3 => 'app_booking_create_step_3',
4 => 'app_booking_create_step_4',
default => 'app_booking_create_step_1',
};
return $this->redirectToRoute($route);
}
}
@@ -4,8 +4,9 @@ declare(strict_types=1);
namespace App\Controller\Booking\Create;
use App\Controller\Booking\Traits\BookingCreateTrait;
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
use App\Exception\BookingSessionNotFoundException;
use App\Exception\HotelNotInTravelException;
use App\Exception\TravelNotFoundException;
use App\Form\BookingCreateStep1Type;
use App\Form\Model\BookingDto;
use App\Htmx\HxTrait;
@@ -13,7 +14,6 @@ use App\Service\BookingConfigurator;
use App\Service\BookingCreateContextFactory;
use App\Service\BookingSessionManager;
use App\Service\RoomPricingCalculator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@@ -24,10 +24,8 @@ use Symfony\Component\Routing\Attribute\Route;
* This controller manages room selection functionality where users
* choose the types and quantities of rooms for their booking.
*/
class Step1Controller extends AbstractController
class Step1Controller extends AbstractBookingCreateController
{
use BookingCreateTrait;
use BookingExceptionHandlerTrait;
use HxTrait;
public function __construct(
@@ -43,11 +41,11 @@ class Step1Controller extends AbstractController
#[Route('/bookings/create/rooms', name: 'app_booking_create_step_1')]
public function index(Request $request): Response
{
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
if ($result instanceof Response) {
return $result;
try {
$bookingCreateDto = $this->loadBookingCreateDto($this->bookingSessionService, $request);
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) {
return $this->createBookingCreateFailureResponse($exception, false);
}
$bookingCreateDto = $result;
// Get or create baseline snapshot for change detection
$oldRoomSelectionSnapshot = $this->bookingSessionService->getOrCreateBaselineSnapshot($request, $bookingCreateDto);
@@ -98,11 +96,11 @@ class Step1Controller extends AbstractController
#[Route('/bookings/create/refresh', name: 'app_booking_create_step_1_refresh', methods: ['POST'])]
public function refresh(Request $request): Response
{
$result = $this->getOrCreateBookingCreateDtoForHtmx($this->bookingSessionService, $request);
if ($result instanceof Response) {
return $result;
try {
$bookingCreateDto = $this->loadBookingCreateDto($this->bookingSessionService, $request);
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) {
return $this->createBookingCreateFailureResponse($exception, true);
}
$bookingCreateDto = $result;
// Process form data without validation to capture current state
$form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto, [
@@ -5,19 +5,20 @@ declare(strict_types=1);
namespace App\Controller\Booking\Create;
use App\BusProNet\Model\Travel;
use App\Controller\Booking\Traits\BookingCreateTrait;
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
use App\Exception\BookingSessionNotFoundException;
use App\Exception\HotelNotInTravelException;
use App\Exception\TravelNotFoundException;
use App\Entity\User;
use App\Form\BookingCreateStep2Type;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Form\Model\RoomSelectionDto;
use App\Service\BookingConfigurator;
use App\Service\BookingCreateContextFactory;
use App\Service\BookingSessionManager;
use App\Service\ParticipantDataPrefiller;
use App\Service\RoomAssigner;
use App\Service\TravelDataProvider;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@@ -28,11 +29,8 @@ use Symfony\Component\Routing\Attribute\Route;
* This controller uses a card overview and lazy-loaded individual participant forms
* for better performance and UX with large groups (50+ participants).
*/
class Step2Controller extends AbstractController
class Step2Controller extends AbstractBookingCreateController
{
use BookingCreateTrait;
use BookingExceptionHandlerTrait;
public function __construct(
private readonly BookingConfigurator $bookingService,
private readonly BookingSessionManager $bookingSessionService,
@@ -49,15 +47,14 @@ class Step2Controller extends AbstractController
#[Route('/bookings/create/participants', name: 'app_booking_create_step_2')]
public function index(Request $request): Response
{
// Load or create booking DTO
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
if ($result instanceof Response) {
return $result;
try {
$bookingCreateDto = $this->loadBookingCreateDto($this->bookingSessionService, $request);
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) {
return $this->createBookingCreateFailureResponse($exception, false);
}
$bookingCreateDto = $result;
// Validate step access
if ($redirect = $this->validateStepAccess($bookingCreateDto, 2)) {
if (null !== $redirect = $this->validateStepAccess($bookingCreateDto, 2)) {
return $redirect;
}
@@ -132,7 +129,9 @@ class Step2Controller extends AbstractController
}
}
/** @param array<int, \App\Form\Model\RoomSelectionDto> $roomSelections */
/**
* @param array<int, RoomSelectionDto> $roomSelections
*/
private function calculateParticipantsCount(array $roomSelections, Travel $travelData): int
{
$participantsCount = 0;
@@ -8,8 +8,9 @@ use App\BusProNet\ApiClient;
use App\BusProNet\Exception\TimeoutException;
use App\BusProNet\Model\BookingResponse;
use App\BusProNet\Model\Notification;
use App\Controller\Booking\Traits\BookingCreateTrait;
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
use App\Exception\BookingSessionNotFoundException;
use App\Exception\HotelNotInTravelException;
use App\Exception\TravelNotFoundException;
use App\Form\BookingCreateStep3Type;
use App\Form\Model\BookingDto;
use App\Htmx\HxTrait;
@@ -20,7 +21,6 @@ use App\Service\BookingPriceMismatchAnalyzer;
use App\Service\BookingSessionManager;
use App\Service\RoomPricingCalculator;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -29,10 +29,8 @@ use Symfony\Component\Routing\Attribute\Route;
/**
* Handles the third step of the booking creation process (payment method selection).
*/
class Step3Controller extends AbstractController
class Step3Controller extends AbstractBookingCreateController
{
use BookingCreateTrait;
use BookingExceptionHandlerTrait;
use HxTrait;
public function __construct(
@@ -52,14 +50,14 @@ class Step3Controller extends AbstractController
#[Route('/bookings/create/payment', name: 'app_booking_create_step_3')]
public function index(Request $request): Response
{
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
if ($result instanceof Response) {
return $result;
try {
$bookingCreateDto = $this->loadBookingCreateDto($this->bookingSessionService, $request);
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) {
return $this->createBookingCreateFailureResponse($exception, false);
}
$bookingCreateDto = $result;
// Validate step access
if ($redirect = $this->validateStepAccess($bookingCreateDto, 3)) {
if (null !== $redirect = $this->validateStepAccess($bookingCreateDto, 3)) {
return $redirect;
}
@@ -203,11 +201,11 @@ 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->bookingSessionService, $request);
if ($result instanceof Response) {
return $result;
try {
$bookingCreateDto = $this->loadBookingCreateDto($this->bookingSessionService, $request);
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) {
return $this->createBookingCreateFailureResponse($exception, true);
}
$bookingCreateDto = $result;
$form = $this->createForm(BookingCreateStep3Type::class, $bookingCreateDto, [
'validation_groups' => false,
@@ -248,7 +246,7 @@ class Step3Controller extends AbstractController
private function shouldFallbackToInquiryMode(BookingResponse $response): bool
{
// Check for "nicht möglich" status + inquiry suggestion in message
if ('nicht möglich' !== $response->status) {
if (BookingResponse::BOOKING_IMPOSSIBLE !== $response->status) {
return false;
}
@@ -7,10 +7,11 @@ namespace App\Controller\Booking\Create;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\TimeoutException;
use App\BusProNet\Model\Notification;
use App\Controller\Booking\Traits\BookingCreateTrait;
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
use App\Entity\User;
use App\Exception\BookingSessionNotFoundException;
use App\Exception\HotelNotInTravelException;
use App\Exception\NewsletterProviderException;
use App\Exception\TravelNotFoundException;
use App\Form\BookingCreateStep4Type;
use App\Form\Model\BookingDto;
use App\Htmx\HxTrait;
@@ -20,7 +21,6 @@ use App\Service\BookingSessionManager;
use App\Service\MailjetApiClient;
use App\Service\NewsletterManager;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -30,10 +30,8 @@ use Symfony\Contracts\Cache\CacheInterface;
/**
* Handles the fourth step of the booking creation process (confirmation).
*/
class Step4Controller extends AbstractController
class Step4Controller extends AbstractBookingCreateController
{
use BookingCreateTrait;
use BookingExceptionHandlerTrait;
use HxTrait;
public function __construct(
@@ -54,14 +52,14 @@ class Step4Controller extends AbstractController
#[Route('/bookings/create/confirmation', name: 'app_booking_create_step_4')]
public function index(Request $request): Response
{
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
if ($result instanceof Response) {
return $result;
try {
$bookingCreateDto = $this->loadBookingCreateDto($this->bookingSessionService, $request);
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) {
return $this->createBookingCreateFailureResponse($exception, false);
}
$bookingCreateDto = $result;
// Validate step access
if ($redirect = $this->validateStepAccess($bookingCreateDto, 4)) {
if (null !== $redirect = $this->validateStepAccess($bookingCreateDto, 4)) {
return $redirect;
}