wip: improved validation and feedback

This commit is contained in:
Björn Fromme
2025-10-23 08:50:45 +02:00
parent 9c716477aa
commit 60176d19f4
8 changed files with 36 additions and 176 deletions
@@ -7,7 +7,6 @@ namespace App\Controller\Booking\Create;
use App\Controller\Booking\Traits\BookingCreateTrait;
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
use App\Controller\Booking\Traits\ParticipantCardFlowTrait;
use App\Controller\Booking\Traits\ParticipantValidationTrait;
use App\Form\BookingCreateStep2Type;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
@@ -35,7 +34,6 @@ class Step2Controller extends AbstractController
use BookingExceptionHandlerTrait;
use HxTrait;
use ParticipantCardFlowTrait;
use ParticipantValidationTrait;
public function __construct(
private readonly BookingService $bookingService,
@@ -82,7 +80,7 @@ class Step2Controller extends AbstractController
// Create validation form
$form = $this->createForm(BookingCreateStep2Type::class, $bookingCreateDto, [
'validation_groups' => ['booking_create_step_2'],
'validation_groups' => ['booking_create'],
]);
$form->handleRequest($request);
@@ -96,15 +94,6 @@ class Step2Controller extends AbstractController
return $this->hxRedirect($request, $this->generateUrl('app_booking_create_step_3'));
}
// Extract participant validation errors
$participantErrorIndices = [];
$participantErrorMessages = [];
if (true === $form->isSubmitted() && false === $form->isValid()) {
$extractedErrors = $this->extractParticipantValidationErrors($form);
$participantErrorIndices = $extractedErrors['errorIndices'];
$participantErrorMessages = $extractedErrors['errorMessages'];
}
// Generate cards data
$cardsData = $this->generateAllCardsData($bookingCreateDto);
@@ -120,8 +109,6 @@ class Step2Controller extends AbstractController
'cardsData' => $cardsData,
'summaryData' => $summaryData,
'pricingData' => $summary['pricing'],
'participantErrors' => $participantErrorIndices,
'participantErrorMessages' => $participantErrorMessages,
];
// HTMX request: render blocks only
@@ -159,7 +146,7 @@ class Step2Controller extends AbstractController
// Create form with booking_context option
$form = $this->createParticipantForm($bookingDto, $index, [
'validation_groups' => ['booking_create_step_2'],
'validation_groups' => ['booking_create'],
]);
$form->handleRequest($request);
@@ -11,7 +11,6 @@ use App\BusProNet\Model\Notification;
use App\Controller\Booking\Traits;
use App\Controller\Booking\Traits\BookingDataTrait;
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
use App\Controller\Booking\Traits\ParticipantValidationTrait;
use App\Entity\User;
use App\Form\BookingEditType;
use App\Form\BookingParticipantType;
@@ -49,7 +48,6 @@ class IndexController extends AbstractController
use BookingExceptionHandlerTrait;
use HxTrait;
use Traits\ParticipantCardFlowTrait;
use ParticipantValidationTrait;
public function __construct(
private readonly ApiClient $apiClient,
@@ -156,15 +154,6 @@ class IndexController extends AbstractController
return $this->hxRedirect($request, $this->generateUrl('app_booking_edit', ['id' => $id]));
}
// Extract participant validation errors
$participantErrors = [];
$participantErrorMessages = [];
if ($form->isSubmitted() && false === $form->isValid()) {
$extractedErrors = $this->extractParticipantValidationErrors($form);
$participantErrors = $extractedErrors['errorIndices'];
$participantErrorMessages = $extractedErrors['errorMessages'];
}
// Generate card data for all participants
$cardsData = $this->participantCardService->getAllCardsData($bookingDto);
@@ -190,9 +179,7 @@ class IndexController extends AbstractController
'groupedSelectedRooms' => $groupedSelectedRooms,
'assignmentCounts' => $roomAssignmentCounts,
'isDirty' => $this->fingerprintService->isDirty($bookingDto),
'hasValidationErrors' => count($participantErrors) > 0,
'participantErrors' => $participantErrors,
'participantErrorMessages' => $participantErrorMessages,
'hasValidationErrors' => $form->isSubmitted() && false === $form->isValid(),
];
// If HTMX request, render only blocks to avoid layout duplication
@@ -1,98 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller\Booking\Traits;
/**
* Provides participant validation error extraction for card-based booking flows.
*
* Shared between CreateStep2Controller and EditController to identify which
* participants have validation errors that should be displayed on their cards.
*/
trait ParticipantValidationTrait
{
/**
* Extracts participant validation errors from form.
*
* Returns two arrays:
* - errorIndices: Array of participant indices with errors (e.g., [0, 2, 5])
* - errorMessages: Map of participant index to error messages (e.g., [0 => ['E-Mail: ...', 'Vorname: ...']])
*
* @return array{errorIndices: array<int>, errorMessages: array<int, array<string>>}
*/
private function extractParticipantValidationErrors($form): array
{
$errorIndices = [];
$errorMessages = [];
$errors = $form->getErrors(true); // Get all errors recursively
foreach ($errors as $error) {
$propertyPath = $error->getCause()?->getPropertyPath();
if (null === $propertyPath) {
continue;
}
// Property paths from BookingDto validation look like "participants[0].email"
// Note: Using more flexible regex to capture field path after index
if (preg_match('/participants\[(\d+)\]\.?(.*)/', (string) $propertyPath, $matches)) {
$index = (int) $matches[1];
$fieldPath = $matches[2] ?? '';
$message = $error->getMessage();
// Mark this participant as having errors
$errorIndices[$index] = true;
// Initialize error messages array for this participant if needed
if (false === isset($errorMessages[$index])) {
$errorMessages[$index] = [];
}
// Add formatted error message
$errorMessages[$index][] = $this->formatErrorMessage($fieldPath, $message);
}
}
return [
'errorIndices' => array_keys($errorIndices),
'errorMessages' => $errorMessages,
];
}
/**
* Formats an error message with field context.
*
* @param string $fieldPath The field path that has the error (may be empty or nested)
* @param string $message The error message
*
* @return string Formatted error message
*/
private function formatErrorMessage(string $fieldPath, string $message): string
{
// If no field path, return just the message (error is on participant level)
if ('' === trim($fieldPath)) {
return $message;
}
// Extract the first part of the path for nested fields (e.g., "address.street" -> "address")
$fieldName = explode('.', $fieldPath)[0];
// Field name translations for better user understanding
$fieldLabels = [
'email' => 'E-Mail',
'firstName' => 'Vorname',
'lastName' => 'Nachname',
'dateOfBirth' => 'Geburtsdatum',
'assignedRoomId' => 'Zimmer',
'skiPass' => 'Skipass',
'transportationOutbound' => 'Anreise',
'transportationInbound' => 'Rückreise',
'mobile' => 'Mobilnummer',
'address' => 'Adresse',
];
$fieldLabel = $fieldLabels[$fieldName] ?? $fieldName;
return sprintf('%s: %s', $fieldLabel, $message);
}
}