feat: split booking participant controllers
This commit is contained in:
1 parent
346256f895
commit
f22ceae41c
10 files changed
+767
-673
No files matched your search
@@ -1,204 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Booking\Traits;
|
||||
|
||||
use App\Form\BookingParticipantType;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantEditDto;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Shared controller logic for participant card-based booking flows.
|
||||
*
|
||||
* This trait provides common functionality for both create and edit controllers
|
||||
* that use the card-based UI pattern (card overview + lazy-loaded forms).
|
||||
*/
|
||||
trait ParticipantCardFlowTrait
|
||||
{
|
||||
/**
|
||||
* Load BookingDto from session or return redirect to login.
|
||||
*
|
||||
* Returns RedirectResponse when session data is missing (e.g., after cancel or session expiry).
|
||||
*/
|
||||
private function loadBookingDtoOrRedirect(Request $request, string $mode): BookingDto|RedirectResponse
|
||||
{
|
||||
$bookingDto = $this->bookingService->getBookingDto($request, $mode);
|
||||
|
||||
if (null === $bookingDto) {
|
||||
$this->addFlash('info', 'Deine Sitzung ist abgelaufen. Bitte starte eine neue Buchung.');
|
||||
|
||||
return $this->redirectToRoute('app_login');
|
||||
}
|
||||
|
||||
return $bookingDto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate card data for all participants.
|
||||
*
|
||||
* @return array<int, array{name: string, roomName: string, price: string}>
|
||||
*/
|
||||
private function generateAllCardsData(BookingDto $bookingDto): array
|
||||
{
|
||||
return $this->participantCardService->getAllCardsData($bookingDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create form for single participant.
|
||||
*
|
||||
* This creates an autonomous participant form with booking_context option
|
||||
* so it can process field handlers independently.
|
||||
*/
|
||||
private function createParticipantForm(
|
||||
BookingDto $bookingDto,
|
||||
int $index,
|
||||
array $options = [],
|
||||
): FormInterface {
|
||||
$participant = $bookingDto->participants[$index] ?? null;
|
||||
|
||||
if (null === $participant) {
|
||||
throw new \InvalidArgumentException(sprintf('Participant at index %d does not exist', $index));
|
||||
}
|
||||
|
||||
// Create wrapper DTO for email uniqueness validation
|
||||
$wrapper = new ParticipantEditDto(
|
||||
participant: $participant,
|
||||
bookingContext: $bookingDto,
|
||||
);
|
||||
|
||||
// Merge default options with provided options and body dimensions
|
||||
$formOptions = array_merge([
|
||||
'booking_context' => $bookingDto,
|
||||
'height_choices' => $this->getParameter('body_dimensions.height_choices'),
|
||||
'weight_choices' => $this->getParameter('body_dimensions.weight_choices'),
|
||||
'shoe_size_min' => $this->getParameter('body_dimensions.shoe_size_min'),
|
||||
'shoe_size_max' => $this->getParameter('body_dimensions.shoe_size_max'),
|
||||
], $options);
|
||||
|
||||
return $this->createForm(BookingParticipantType::class, $wrapper, $formOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects notifications from all participants and clears them from DTOs.
|
||||
*
|
||||
* This is important for scenarios where field handlers add notifications
|
||||
* during form processing (e.g., voucher validation, auto-unassignment).
|
||||
* Notifications from ALL participants are collected, not just the current one,
|
||||
* because cross-participant logic may add notifications to multiple participants.
|
||||
*
|
||||
* Note: ParticipantDto stores notifications with MD5 keys to prevent duplicates,
|
||||
* but the JavaScript toast controller expects a simple indexed array. We use
|
||||
* array_values() to convert the associative array to an indexed array.
|
||||
*
|
||||
* @return array<array{type: string, message: string}> Array of notification messages
|
||||
*/
|
||||
private function collectAndClearNotifications(BookingDto $bookingDto): array
|
||||
{
|
||||
$notifications = [];
|
||||
foreach ($bookingDto->participants as $participant) {
|
||||
if (false === empty($participant->notifications)) {
|
||||
$notifications = array_merge($notifications, $participant->notifications);
|
||||
$participant->notifications = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Convert MD5-keyed associative array to simple indexed array
|
||||
return array_values($notifications);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process single participant form refresh.
|
||||
*
|
||||
* Handles HTMX form refresh without validation, updates sidebar via OOB swap.
|
||||
*/
|
||||
private function handleParticipantRefresh(
|
||||
Request $request,
|
||||
BookingDto $bookingDto,
|
||||
int $index,
|
||||
string $refreshRouteName,
|
||||
): Response {
|
||||
$refreshFormOptions = ['validation_groups' => false];
|
||||
|
||||
// Create form with validation disabled
|
||||
$form = $this->createParticipantForm($bookingDto, $index, $refreshFormOptions);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
// Refresh endpoint is POST-only and always processes submitted participant data.
|
||||
$this->bookingService->preselectDefaultServices($bookingDto);
|
||||
if (BookingDto::MODE_CREATE === $bookingDto->getMode()) {
|
||||
$this->bookingService->applyCreateBookingStatusRules($bookingDto);
|
||||
}
|
||||
|
||||
// Recreate form so the rendered state reflects any new auto-preselections.
|
||||
$form = $this->createParticipantForm($bookingDto, $index, $refreshFormOptions);
|
||||
|
||||
// Collect notifications from field handlers
|
||||
$notifications = $this->collectAndClearNotifications($bookingDto);
|
||||
|
||||
// Save updated booking data to session (after collecting & clearing notifications)
|
||||
$this->bookingService->saveBookingDto($request, $bookingDto, $bookingDto->getMode());
|
||||
|
||||
// Get complete summary data (pricing, rooms, CMS data)
|
||||
$summaryData = $this->summaryDataService->getSummaryData($bookingDto);
|
||||
|
||||
// Render form and sidebar with OOB swap using htmxOobResponse
|
||||
// This renders ONLY the specific blocks, not the entire template
|
||||
$response = $this->htmxOobResponse(
|
||||
'booking/_participant_form.html.twig',
|
||||
['participant_form', 'booking_summary'],
|
||||
[
|
||||
'form' => $form->createView(),
|
||||
'participantIndex' => $index,
|
||||
'bookingDto' => $bookingDto,
|
||||
'summaryData' => $summaryData,
|
||||
'refreshRouteName' => $refreshRouteName,
|
||||
]
|
||||
);
|
||||
|
||||
// Add notifications to HX-Trigger header if present
|
||||
if (false === empty($notifications)) {
|
||||
$response->headers->set('HX-Trigger', json_encode([
|
||||
'showNotifications' => $notifications,
|
||||
]));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts collected notifications to flash messages.
|
||||
*
|
||||
* Used when redirecting after form submission, as HTMX-triggered toasts
|
||||
* are destroyed on redirect. Flash messages persist across the redirect.
|
||||
*
|
||||
* @param array<array{type: string, message: string}> $notifications The notifications to convert
|
||||
*/
|
||||
private function addNotificationsAsFlashMessages(array $notifications): void
|
||||
{
|
||||
foreach ($notifications as $notification) {
|
||||
$this->addFlash($notification['type'], $notification['message']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Required services - implementing controllers must inject these.
|
||||
*
|
||||
* Controllers using this trait must have the following properties:
|
||||
* - BookingService $bookingService
|
||||
* - ParticipantCardDataService $participantCardService
|
||||
* - BookingSummaryDataService $summaryDataService
|
||||
*/
|
||||
abstract private function createForm(string $type, $data = null, array $options = []): FormInterface;
|
||||
|
||||
abstract private function render(string $view, array $parameters = [], ?Response $response = null): Response;
|
||||
|
||||
abstract protected function addFlash(string $type, mixed $message): void;
|
||||
|
||||
abstract protected function redirectToRoute(string $route, array $parameters = [], int $status = 302): RedirectResponse;
|
||||
}
|
||||
Reference in new issue
Block a user