wip: refactor

This commit is contained in:
Björn Fromme
2025-07-24 11:12:06 +02:00
parent 64d51c01b2
commit 3f94c51bc8
21 changed files with 395 additions and 110 deletions
@@ -33,7 +33,7 @@ class CreateStep1Controller extends AbstractController
public function index(Request $request): Response
{
$bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request);
$participantsCount = $this->bookingService->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travelData);
$participantsCount = $this->bookingService->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travel);
// Validate step access - allow step 1 or redirect to current step
$this->validateStepAccess($bookingCreateDto, 1);
@@ -112,7 +112,7 @@ class CreateStep2Controller extends AbstractController
{
return $this
->bookingService
->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travelData);
->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travel);
}
private function ensureCorrectNumberOfParticipants(BookingCreateDto $bookingCreateDto): void
+16 -15
View File
@@ -5,6 +5,7 @@ namespace App\Form;
use App\BusProNet\Form\CountryType;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\ParticipantDto;
use App\Form\Model\BookingDtoInterface;
use App\Form\Service\ParticipantFieldOptionsProvider;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
@@ -90,11 +91,11 @@ class BookingCreateParticipantType extends AbstractType
$rootForm = $rootForm->getParent();
}
/** @var BookingCreateDto $bookingCreateDto */
$bookingCreateDto = $rootForm->getData();
/** @var BookingDtoInterface $bookingDto */
$bookingDto = $rootForm->getData();
$this->addDynamicFields($form, $bookingCreateDto, $participantData->index);
$this->applyFieldStates($form, $bookingCreateDto, $participantData->index);
$this->addDynamicFields($form, $bookingDto, $participantData->index);
$this->applyFieldStates($form, $bookingDto, $participantData->index);
}
/**
@@ -109,16 +110,16 @@ class BookingCreateParticipantType extends AbstractType
return;
}
// Get the root form data to access BookingCreateDto
// Get the root form data to access BookingDtoInterface
$rootForm = $form;
while ($rootForm->getParent()) {
$rootForm = $rootForm->getParent();
}
/** @var BookingCreateDto $bookingCreateDto */
$bookingCreateDto = $rootForm->getData();
/** @var BookingDtoInterface $bookingDto */
$bookingDto = $rootForm->getData();
if (null === $bookingCreateDto) {
if (null === $bookingDto) {
return;
}
@@ -129,7 +130,7 @@ class BookingCreateParticipantType extends AbstractType
}
// Apply updated field states based on submitted data
$this->applyFieldStates($form, $bookingCreateDto, $participantData->index, $submittedData);
$this->applyFieldStates($form, $bookingDto, $participantData->index, $submittedData);
}
/**
@@ -139,14 +140,14 @@ class BookingCreateParticipantType extends AbstractType
* the appropriate state modifications (readonly, disabled, etc.).
*
* @param FormInterface $form The form to modify
* @param BookingCreateDto $bookingCreateDto The booking data for context
* @param BookingDtoInterface $bookingDto The booking data for context (create or edit)
* @param int $participantIndex The participant index
* @param array<string, mixed> $formData Optional submitted form data for state calculation
*/
private function applyFieldStates(FormInterface $form, BookingCreateDto $bookingCreateDto, int $participantIndex, array $formData = []): void
private function applyFieldStates(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): void
{
// Get all fields that have state conditions
$allFieldStates = $this->fieldOptionsProvider->getAllFieldStates($bookingCreateDto, $participantIndex, $formData);
$allFieldStates = $this->fieldOptionsProvider->getAllFieldStates($bookingDto, $participantIndex, $formData);
foreach ($allFieldStates as $fieldName => $fieldState) {
if ($form->has($fieldName)) {
@@ -167,18 +168,18 @@ class BookingCreateParticipantType extends AbstractType
/**
* Adds all configured dynamic fields to the form with state conditions applied.
*/
private function addDynamicFields(FormInterface $form, BookingCreateDto $bookingCreateDto, int $participantIndex): void
private function addDynamicFields(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex): void
{
$dynamicFields = ['assignedRoomId']; // List of fields that need dynamic configuration
foreach ($dynamicFields as $fieldName) {
if ($this->fieldOptionsProvider->hasFieldOptions($fieldName)) {
// Get base field options
$fieldOptions = $this->fieldOptionsProvider->getFieldOptions($fieldName, $bookingCreateDto, $participantIndex);
$fieldOptions = $this->fieldOptionsProvider->getFieldOptions($fieldName, $bookingDto, $participantIndex);
// Apply dynamic field state if conditions exist
if ($this->fieldOptionsProvider->hasStateConditions($fieldName)) {
$fieldState = $this->fieldOptionsProvider->getFieldState($fieldName, $bookingCreateDto, $participantIndex);
$fieldState = $this->fieldOptionsProvider->getFieldState($fieldName, $bookingDto, $participantIndex);
$fieldOptions = $this->mergeFieldState($fieldOptions, $fieldState);
}
+84 -39
View File
@@ -7,6 +7,8 @@ use App\BusProNet\Form\CountryType;
use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\Service;
use App\Form\Model\ParticipantDto;
use App\Form\Model\BookingDtoInterface;
use App\Form\Service\EditFieldStateProvider;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
@@ -19,6 +21,10 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class BookingEditParticipantType extends AbstractType
{
public function __construct(
private readonly EditFieldStateProvider $fieldStateProvider,
) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
@@ -33,34 +39,33 @@ class BookingEditParticipantType extends AbstractType
$form = $event->getForm();
$personalDataMutable = $options['personal_data_mutable'] && $participantData->mutable;
$isApplicant = 0 === $participantIndex;
// Traverse up the form tree to get the root form's data (BookingEditDto)
$rootForm = $form;
while ($rootForm->getParent()) {
$rootForm = $rootForm->getParent();
}
/** @var BookingDtoInterface $bookingDto */
$bookingDto = $rootForm->getData();
// Helper to get state for a field
$getState = fn(string $field) => $this->fieldStateProvider->getFieldState($field, $bookingDto, $participantIndex);
$form
->add('firstName', TextType::class, [
->add('firstName', TextType::class, $this->mergeFieldState([
'label' => 'Vorname',
'attr' => [
'readonly' => false === $personalDataMutable,
],
'clean_xss' => true,
])
->add('lastName', TextType::class, [
], $getState('firstName')))
->add('lastName', TextType::class, $this->mergeFieldState([
'label' => 'Nachname',
'attr' => [
'readonly' => false === $personalDataMutable,
],
'clean_xss' => true,
])
->add('dateOfBirth', BirthdayType::class, [
], $getState('lastName')))
->add('dateOfBirth', BirthdayType::class, $this->mergeFieldState([
'label' => 'Geburtsdatum',
'html5' => true,
'widget' => 'single_text',
'input' => 'datetime_immutable',
'attr' => [
'readonly' => false === $personalDataMutable,
],
])
->add('gender', ChoiceType::class, [
], $getState('dateOfBirth')))
->add('gender', ChoiceType::class, $this->mergeFieldState([
'label' => 'Geschlecht',
'required' => false,
'placeholder' => 'keine Angabe',
@@ -69,36 +74,22 @@ class BookingEditParticipantType extends AbstractType
'weiblich' => 'W',
'divers' => 'D',
],
'attr' => [
'readonly' => false === $personalDataMutable,
'style' => false === $personalDataMutable ? 'pointer-events: none' : null,
],
])
->add('nationality', CountryType::class, [
], $getState('gender')))
->add('nationality', CountryType::class, $this->mergeFieldState([
'label' => 'Nationalität',
'property' => 'nationality',
'preferred_choices' => ['D', 'A', 'CH'],
'attr' => [
'readonly' => false === $personalDataMutable,
'style' => false === $personalDataMutable ? 'pointer-events: none' : null,
],
])
->add('email', EmailType::class, [
], $getState('nationality')))
->add('email', EmailType::class, $this->mergeFieldState([
'label' => 'E-Mail',
'required' => false,
'attr' => [
'readonly' => true === $isApplicant,
],
'clean_xss' => true,
])
->add('mobile', TextType::class, [
], $getState('email')))
->add('mobile', TextType::class, $this->mergeFieldState([
'label' => 'Telefon (mobil)',
'required' => false,
'attr' => [
'readonly' => true === $isApplicant,
],
'clean_xss' => true,
])
], $getState('mobile')))
->add('bodyDimensions', BodyDimensionsType::class);
$commonChoiceFieldOptions = [
@@ -294,6 +285,33 @@ class BookingEditParticipantType extends AbstractType
return;
}
// Traverse up the form tree to get the root form's data (BookingEditDto)
$rootForm = $form;
while ($rootForm->getParent()) {
$rootForm = $rootForm->getParent();
}
/** @var BookingDtoInterface $bookingDto */
$bookingDto = $rootForm->getData();
$participantIndex = $form->getData()->index;
// Helper to get state for a field based on submitted data
$getState = fn(string $field) => $this->fieldStateProvider->getFieldState($field, $bookingDto, $participantIndex, $data);
// Re-apply field states to all personal data fields
$personalDataFields = [
'firstName', 'lastName', 'dateOfBirth', 'gender', 'nationality', 'email', 'mobile'
];
foreach ($personalDataFields as $field) {
if ($form->has($field)) {
$fieldConfig = $form->get($field)->getConfig();
$currentOptions = $fieldConfig->getOptions();
$updatedOptions = $this->mergeFieldState($currentOptions, $getState($field));
$fieldType = $fieldConfig->getType()->getInnerType();
$form->remove($field);
$form->add($field, $fieldType::class, $updatedOptions);
}
}
$transportationId = $data['transportationServiceTo'] ?? null;
$transportation = $options['travel']->pickups[$transportationId] ?? null;
@@ -344,6 +362,33 @@ class BookingEditParticipantType extends AbstractType
'transportation_services_mutable' => true,
'pickups_mutable' => true,
'applicant_id' => null,
// Booking DTO for field state provider (must be set by parent form)
'booking' => null,
]);
}
/**
* Merges field state modifications into existing field options.
*
* This method combines the base field options with dynamic state modifications,
* handling attribute merging and option overrides correctly.
*
* @param array<string, mixed> $fieldOptions The base field options
* @param array<string, mixed> $fieldState The dynamic state modifications
*
* @return array<string, mixed> The merged field options with state applied
*/
private function mergeFieldState(array $fieldOptions, array $fieldState): array
{
foreach ($fieldState as $key => $value) {
if ('attr' === $key && isset($fieldOptions['attr'])) {
// Merge attributes instead of overwriting
$fieldOptions['attr'] = array_merge($fieldOptions['attr'], $value);
} else {
// Direct assignment for non-attribute options
$fieldOptions[$key] = $value;
}
}
return $fieldOptions;
}
}
+7
View File
@@ -47,4 +47,11 @@ interface BookingDtoInterface
* @return ParticipantDto|null The participant DTO or null if not found
*/
public function getParticipant(int $index): ?ParticipantDto;
/**
* Gets all selected rooms for the booking.
*
* @return array<int, RoomSelectionDto> Array of selected room DTOs (may be empty for edit DTOs)
*/
public function getSelectedRooms(): array;
}
+10
View File
@@ -83,4 +83,14 @@ class BookingEditDto implements BookingDtoInterface
{
return $this->participants[$index] ?? null;
}
/**
* Gets all selected rooms for the booking (edit context).
*
* @return array<int, RoomSelectionDto> Always returns an empty array for edit DTOs unless implemented.
*/
public function getSelectedRooms(): array
{
return [];
}
}
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/**
* Abstract base class providing common functionality for participant field handlers.
@@ -54,14 +54,15 @@ abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandle
* if the participant exists at the given index. Returns null if the
* participant doesn't exist, preventing array access errors.
*
* @param BookingCreateDto $bookingDto The booking DTO containing participants
* @param int $participantIndex The index of the participant to retrieve
* @param BookingDtoInterface $bookingDto The booking DTO containing participants (create or edit)
* @param int $participantIndex The index of the participant to retrieve
*
* @return object|null The participant object, or null if not found
*/
protected function getParticipant(BookingCreateDto $bookingDto, int $participantIndex): ?object
protected function getParticipant(BookingDtoInterface $bookingDto, int $participantIndex): ?object
{
return $bookingDto->participants[$participantIndex] ?? null;
$participants = $bookingDto->getParticipants();
return $participants[$participantIndex] ?? null;
}
/**
@@ -126,12 +127,12 @@ abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandle
* processing results.
*
* @param array<string, mixed> $submittedData The submitted participant form data
* @param BookingCreateDto $bookingDto The booking DTO (potentially modified by processing)
* @param BookingDtoInterface $bookingDto The booking DTO (potentially modified by processing)
* @param int $participantIndex The participant index being processed
*
* @return array<string, array<string, mixed>> Empty array (no state modifications by default)
*/
public function getFieldStateModifications(array $submittedData, BookingCreateDto $bookingDto, int $participantIndex): array
public function getFieldStateModifications(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): array
{
return [];
}
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/**
* Condition that evaluates participant age against specified range criteria.
@@ -53,15 +53,15 @@ class AgeRangeCondition implements FieldConditionInterface
* checks if it falls within the configured age range. Returns false if
* the participant has no date of birth set.
*
* @param BookingCreateDto $bookingDto The current booking data
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data (unused for age conditions)
*
* @return bool True if the participant's age meets the criteria, false otherwise
*/
public function evaluate(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
{
$participant = $bookingDto->participants[$participantIndex] ?? null;
$participant = $bookingDto->getParticipant($participantIndex);
if (null === $participant || null === $participant->dateOfBirth) {
return false;
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition;
use App\Form\Model\BookingDtoInterface;
/**
* Condition that checks if the participant is the applicant.
*
* This is used to apply field state logic (e.g., enable/disable fields)
* specifically for the applicant participant in the booking.
*
* The default logic assumes the applicant is the first participant (index 0),
* but this can be adjusted if your domain uses a different rule or property.
*/
class ApplicantCondition implements FieldConditionInterface
{
/**
* Evaluates if the participant is the applicant.
*
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data (unused)
*
* @return bool True if the participant is the applicant, false otherwise
*/
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
{
// Default: applicant is the first participant (index 0)
return $participantIndex === 0;
}
public function getDependentFields(): array
{
return [];
}
public function getDescription(): string
{
return 'Checks if the participant is the applicant (index 0)';
}
}
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/**
* Composite condition that combines multiple conditions with logical operators.
@@ -63,13 +63,13 @@ class CompositeCondition implements FieldConditionInterface
* evaluation for optimal performance. The evaluation stops as soon as the
* final result can be determined.
*
* @param BookingCreateDto $bookingDto The current booking data
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation
*
* @return bool True if the composite condition is met, false otherwise
*/
public function evaluate(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
{
return match ($this->operator) {
self::OPERATOR_AND => $this->evaluateAnd($bookingDto, $participantIndex, $formData),
@@ -167,7 +167,7 @@ class CompositeCondition implements FieldConditionInterface
* Returns false as soon as any condition evaluates to false,
* avoiding unnecessary evaluation of remaining conditions.
*/
private function evaluateAnd(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool
private function evaluateAnd(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
{
foreach ($this->conditions as $condition) {
if (!$condition->evaluate($bookingDto, $participantIndex, $formData)) {
@@ -184,7 +184,7 @@ class CompositeCondition implements FieldConditionInterface
* Returns true as soon as any condition evaluates to true,
* avoiding unnecessary evaluation of remaining conditions.
*/
private function evaluateOr(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool
private function evaluateOr(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
{
foreach ($this->conditions as $condition) {
if ($condition->evaluate($bookingDto, $participantIndex, $formData)) {
@@ -198,7 +198,7 @@ class CompositeCondition implements FieldConditionInterface
/**
* Evaluates NOT logic by inverting the result of the single condition.
*/
private function evaluateNot(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool
private function evaluateNot(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
{
return !$this->conditions[0]->evaluate($bookingDto, $participantIndex, $formData);
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/**
* Interface for evaluating field state conditions.
@@ -29,13 +30,13 @@ interface FieldConditionInterface
* state of the booking, participant data, and submitted form values.
* The result is used to determine field state (enabled/disabled/readonly).
*
* @param BookingCreateDto $bookingDto The current booking data
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data (may include partial submissions)
*
* @return bool True if the condition is met, false otherwise
*/
public function evaluate(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool;
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool;
/**
* Returns field names that trigger re-evaluation of this condition.
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/**
* Condition that evaluates field states based on other field values.
@@ -59,13 +59,13 @@ class FieldValueCondition implements FieldConditionInterface
* against the expected value using the configured operator. Supports
* both participant-level fields and booking-level fields.
*
* @param BookingCreateDto $bookingDto The current booking data
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation
*
* @return bool True if the field value meets the condition criteria, false otherwise
*/
public function evaluate(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
{
$fieldValue = $this->getFieldValue($formData, $participantIndex, $bookingDto);
@@ -174,7 +174,7 @@ class FieldValueCondition implements FieldConditionInterface
/**
* Retrieves field value from form data or participant data.
*/
private function getFieldValue(array $formData, int $participantIndex, BookingCreateDto $bookingDto): mixed
private function getFieldValue(array $formData, int $participantIndex, BookingDtoInterface $bookingDto): mixed
{
// First check participant-specific form data
if (isset($formData['participants'][$participantIndex][$this->fieldName])) {
@@ -182,7 +182,7 @@ class FieldValueCondition implements FieldConditionInterface
}
// Then check participant DTO data
$participant = $bookingDto->participants[$participantIndex] ?? null;
$participant = $bookingDto->getParticipant($participantIndex);
if (null !== $participant && property_exists($participant, $this->fieldName)) {
return $participant->{$this->fieldName};
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition;
use App\Form\Model\BookingDtoInterface;
/**
* Condition that checks if a participant's personal data is mutable in the edit flow.
*
* This is used to set fields as readonly or disabled if the booking or participant
* is not allowed to be modified (e.g., after a certain workflow step or status).
*
* The logic assumes the BookingDtoInterface or its participant DTOs expose a
* 'personalDataMutable' property or method. Adjust as needed for your domain.
*/
class MutabilityCondition implements FieldConditionInterface
{
/**
* Evaluates if the participant's personal data is mutable.
*
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data (unused)
*
* @return bool True if the participant's data is mutable, false otherwise
*/
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
{
$participant = $bookingDto->getParticipant($participantIndex);
if (null === $participant) {
return false;
}
return $participant->mutable;
}
public function getDependentFields(): array
{
return [];
}
public function getDescription(): string
{
return 'Checks if the participant\'s personal data is mutable (edit flow)';
}
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/**
* Handles processing of the assignedRoomId field for booking participants.
@@ -52,10 +53,10 @@ class ParticipantAssignedRoomFieldHandler extends AbstractParticipantFieldHandle
* 4. Updates the participant's assignedRoomId property
*
* @param array<string, mixed> $submittedData The submitted participant form data
* @param BookingCreateDto $bookingDto The booking DTO to update
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
* @param int $participantIndex The index of the participant being processed
*/
public function processField(array $submittedData, BookingCreateDto $bookingDto, int $participantIndex): void
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
{
// Safely get the participant object, returning early if not found
$participant = $this->getParticipant($bookingDto, $participantIndex);
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/**
* Interface for handling dynamic participant form field processing and state modification.
@@ -32,10 +32,10 @@ interface ParticipantFieldHandlerInterface
* Processes the participant field data from submitted form data and updates the DTO.
*
* @param array<string, mixed> $submittedData The submitted participant form data
* @param BookingCreateDto $bookingDto The booking DTO to update
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
* @param int $participantIndex The participant index being processed
*/
public function processField(array $submittedData, BookingCreateDto $bookingDto, int $participantIndex): void;
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void;
/**
* Determines if this handler should process the field based on submitted participant data.
@@ -50,12 +50,12 @@ interface ParticipantFieldHandlerInterface
* to enable/disable/hide fields based on the handler's processing results.
*
* @param array<string, mixed> $submittedData The submitted participant form data
* @param BookingCreateDto $bookingDto The booking DTO (potentially modified by processing)
* @param BookingDtoInterface $bookingDto The booking DTO (potentially modified by processing)
* @param int $participantIndex The participant index being processed
*
* @return array<string, array<string, mixed>> Field state modifications indexed by field name
*/
public function getFieldStateModifications(array $submittedData, BookingCreateDto $bookingDto, int $participantIndex): array;
public function getFieldStateModifications(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): array;
/**
* Returns field names whose state is affected by this handler's processing.
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/**
* Registry for managing and executing participant field handlers in dependency order.
@@ -70,9 +71,9 @@ class ParticipantFieldHandlerRegistry
* participant's data and updates the booking DTO accordingly.
*
* @param array<string, mixed> $submittedData The submitted form data containing participants array
* @param BookingCreateDto $bookingDto The booking DTO to update with processed field values
* @param BookingDtoInterface $bookingDto The booking DTO to update with processed field values (create or edit)
*/
public function processFields(array $submittedData, BookingCreateDto $bookingDto): void
public function processFields(array $submittedData, BookingDtoInterface $bookingDto): void
{
// Early return if no participant data exists in submission
if (!isset($submittedData['participants']) || !is_array($submittedData['participants'])) {
+126
View File
@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace App\Form\Service;
use App\Form\Model\BookingDtoInterface;
use App\Form\ParticipantFieldHandler\Condition\FieldConditionInterface;
use App\Form\ParticipantFieldHandler\Condition\MutabilityCondition;
use App\Form\ParticipantFieldHandler\Condition\ApplicantCondition;
use App\Form\ParticipantFieldHandler\Condition\CompositeCondition;
/**
* Field state provider for the booking edit workflow.
*
* This service calculates dynamic field states (readonly, disabled, etc.)
* for participant fields in the edit flow, using edit-specific conditions.
*
* It is designed to be extensible and composable, allowing reuse of
* existing condition classes and easy registration of new logic.
*/
class EditFieldStateProvider implements FieldStateProviderInterface
{
/** @var array<string, array<string, FieldConditionInterface>> */
private array $fieldStateConditions = [];
public function __construct()
{
$this->registerFieldStateConditions();
}
/**
* Registers field state conditions for the edit workflow.
*
* Add or modify conditions as needed for your domain.
*/
private function registerFieldStateConditions(): void
{
// Make all personal data fields readonly if not mutable OR if applicant
$personalDataFields = [
'firstName',
'lastName',
'dateOfBirth',
'gender',
'nationality',
'email',
'mobile',
];
foreach ($personalDataFields as $field) {
$this->fieldStateConditions[$field] = [
'readonly' => CompositeCondition::or(
new ApplicantCondition(),
new MutabilityCondition()
),
];
}
// Example: Only applicant can edit email
$this->fieldStateConditions['email']['readonly'] = new ApplicantCondition();
}
public function getFieldState(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array
{
if (!isset($this->fieldStateConditions[$fieldName])) {
return [];
}
$stateModifications = [];
$attributes = [];
foreach ($this->fieldStateConditions[$fieldName] as $stateType => $condition) {
if ($condition->evaluate($bookingDto, $participantIndex, $formData)) {
switch ($stateType) {
case 'readonly':
$attributes['readonly'] = true;
break;
case 'disabled':
$stateModifications['disabled'] = true;
break;
case 'required':
$stateModifications['required'] = true;
break;
case 'hidden':
$attributes['style'] = ($attributes['style'] ?? '').' display: none;';
break;
}
}
}
if (!empty($attributes)) {
$stateModifications['attr'] = $attributes;
}
return $stateModifications;
}
public function hasStateConditions(string $fieldName): bool
{
return isset($this->fieldStateConditions[$fieldName]) && !empty($this->fieldStateConditions[$fieldName]);
}
public function getFieldStateDependencies(string $fieldName): array
{
if (!isset($this->fieldStateConditions[$fieldName])) {
return [];
}
$dependencies = [];
foreach ($this->fieldStateConditions[$fieldName] as $condition) {
$dependencies = array_merge($dependencies, $condition->getDependentFields());
}
return array_unique($dependencies);
}
public function getAllFieldStates(BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array
{
$allStates = [];
foreach (array_keys($this->fieldStateConditions) as $fieldName) {
$fieldState = $this->getFieldState($fieldName, $bookingDto, $participantIndex, $formData);
if (!empty($fieldState)) {
$allStates[$fieldName] = $fieldState;
}
}
return $allStates;
}
}
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Form\Service;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/**
* Interface for providing dynamic field state based on conditions.
@@ -46,13 +46,13 @@ interface FieldStateProviderInterface
* - 'attr' => ['class' => 'conditional-field'] - Add CSS classes
*
* @param string $fieldName The name of the field to evaluate
* @param BookingCreateDto $bookingDto The current booking data for context
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data (may include partial submissions)
*
* @return array<string, mixed> Symfony form field options for state modifications, empty if no changes needed
*/
public function getFieldState(string $fieldName, BookingCreateDto $bookingDto, int $participantIndex, array $formData = []): array;
public function getFieldState(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array;
/**
* Checks whether a field has state conditions configured.
@@ -91,11 +91,11 @@ interface FieldStateProviderInterface
* when multiple field states need to be determined simultaneously. It's
* particularly useful during form building and bulk state updates.
*
* @param BookingCreateDto $bookingDto The current booking data for context
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation
*
* @return array<string, array<string, mixed>> Field states indexed by field name
*/
public function getAllFieldStates(BookingCreateDto $bookingDto, int $participantIndex, array $formData = []): array;
public function getAllFieldStates(BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array;
}
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Form\Service;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
use App\Form\ParticipantFieldHandler\Condition\FieldConditionInterface;
/**
@@ -64,13 +64,13 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
* - 'disabled' - Whether the field should be disabled
* - 'required' - Whether the field is required
*
* @param string $fieldName The name of the field to configure
* @param BookingCreateDto $bookingDto The current booking data for context
* @param int $participantIndex The index of the participant being configured
* @param string $fieldName The name of the field to configure
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
* @param int $participantIndex The index of the participant being configured
*
* @return array<string, mixed> Symfony form field options, or empty array if field not supported
*/
public function getFieldOptions(string $fieldName, BookingCreateDto $bookingDto, int $participantIndex): array
public function getFieldOptions(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex): array
{
// Check if we have a provider for this field
if (!isset($this->fieldOptionProviders[$fieldName])) {
@@ -121,7 +121,7 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
private function registerFieldOptionProviders(): void
{
// Room assignment field provider
$this->fieldOptionProviders['assignedRoomId'] = fn (BookingCreateDto $bookingDto, int $participantIndex) => [
$this->fieldOptionProviders['assignedRoomId'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
'label' => 'Zimmer',
'placeholder' => 'Bitte wählen',
// Use factory to create context-aware choice loader that:
@@ -155,13 +155,13 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
* disabled, etc.) and evaluated independently.
*
* @param string $fieldName The name of the field to evaluate
* @param BookingCreateDto $bookingDto The current booking data for context
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation
*
* @return array<string, mixed> Symfony form field options for state modifications
*/
public function getFieldState(string $fieldName, BookingCreateDto $bookingDto, int $participantIndex, array $formData = []): array
public function getFieldState(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array
{
if (!isset($this->fieldStateConditions[$fieldName])) {
return [];
@@ -233,13 +233,13 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
/**
* Calculates field states for all configured fields at once.
*
* @param BookingCreateDto $bookingDto The current booking data for context
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation
*
* @return array<string, array<string, mixed>> Field states indexed by field name
*/
public function getAllFieldStates(BookingCreateDto $bookingDto, int $participantIndex, array $formData = []): array
public function getAllFieldStates(BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array
{
$allStates = [];
+3 -3
View File
@@ -16,9 +16,9 @@
</div>
<div>
<h3>Zusammenfassung</h3>
<p>Reise: {{ bookingCreateDto.travelData.label }}</p>
<p>Datum: {{ bookingCreateDto.travelData.dateFrom | date('d.m.Y') }} - {{ bookingCreateDto.travelData.dateTo | date('d.m.Y') }}</p>
<p>Hotel: {{ bookingCreateDto.travelData.hotel.name }}</p>
<p>Reise: {{ bookingCreateDto.travel.label }}</p>
<p>Datum: {{ bookingCreateDto.travel.dateFrom | date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo | date('d.m.Y') }}</p>
<p>Hotel: {{ bookingCreateDto.travel.hotel.name }}</p>
{% if participantsCount > 0 %}
<p>Teilnehmerzahl: {{ participantsCount }}</p>
{% endif %}
+3 -3
View File
@@ -71,19 +71,19 @@
Reise
</h4>
<p>
{{ bookingCreateDto.travelData.label }}
{{ bookingCreateDto.travel.label }}
</p>
<h4>
Datum
</h4>
<p>
{{ bookingCreateDto.travelData.dateFrom | date('d.m.Y') }} - {{ bookingCreateDto.travelData.dateTo | date('d.m.Y') }}
{{ bookingCreateDto.travel.dateFrom | date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo | date('d.m.Y') }}
</p>
<h4>
Unterkunft
</h4>
<p>
{{ bookingCreateDto.travelData.hotel.name }}
{{ bookingCreateDto.travel.hotel.name }}
</p>
{% if participantsCount > 0 %}
<h4>