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 public function index(Request $request): Response
{ {
$bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request); $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 // Validate step access - allow step 1 or redirect to current step
$this->validateStepAccess($bookingCreateDto, 1); $this->validateStepAccess($bookingCreateDto, 1);
@@ -112,7 +112,7 @@ class CreateStep2Controller extends AbstractController
{ {
return $this return $this
->bookingService ->bookingService
->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travelData); ->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travel);
} }
private function ensureCorrectNumberOfParticipants(BookingCreateDto $bookingCreateDto): void private function ensureCorrectNumberOfParticipants(BookingCreateDto $bookingCreateDto): void
+16 -15
View File
@@ -5,6 +5,7 @@ namespace App\Form;
use App\BusProNet\Form\CountryType; use App\BusProNet\Form\CountryType;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingCreateDto;
use App\Form\Model\ParticipantDto; use App\Form\Model\ParticipantDto;
use App\Form\Model\BookingDtoInterface;
use App\Form\Service\ParticipantFieldOptionsProvider; use App\Form\Service\ParticipantFieldOptionsProvider;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\BirthdayType; use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
@@ -90,11 +91,11 @@ class BookingCreateParticipantType extends AbstractType
$rootForm = $rootForm->getParent(); $rootForm = $rootForm->getParent();
} }
/** @var BookingCreateDto $bookingCreateDto */ /** @var BookingDtoInterface $bookingDto */
$bookingCreateDto = $rootForm->getData(); $bookingDto = $rootForm->getData();
$this->addDynamicFields($form, $bookingCreateDto, $participantData->index); $this->addDynamicFields($form, $bookingDto, $participantData->index);
$this->applyFieldStates($form, $bookingCreateDto, $participantData->index); $this->applyFieldStates($form, $bookingDto, $participantData->index);
} }
/** /**
@@ -109,16 +110,16 @@ class BookingCreateParticipantType extends AbstractType
return; return;
} }
// Get the root form data to access BookingCreateDto // Get the root form data to access BookingDtoInterface
$rootForm = $form; $rootForm = $form;
while ($rootForm->getParent()) { while ($rootForm->getParent()) {
$rootForm = $rootForm->getParent(); $rootForm = $rootForm->getParent();
} }
/** @var BookingCreateDto $bookingCreateDto */ /** @var BookingDtoInterface $bookingDto */
$bookingCreateDto = $rootForm->getData(); $bookingDto = $rootForm->getData();
if (null === $bookingCreateDto) { if (null === $bookingDto) {
return; return;
} }
@@ -129,7 +130,7 @@ class BookingCreateParticipantType extends AbstractType
} }
// Apply updated field states based on submitted data // 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.). * the appropriate state modifications (readonly, disabled, etc.).
* *
* @param FormInterface $form The form to modify * @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 int $participantIndex The participant index
* @param array<string, mixed> $formData Optional submitted form data for state calculation * @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 // 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) { foreach ($allFieldStates as $fieldName => $fieldState) {
if ($form->has($fieldName)) { if ($form->has($fieldName)) {
@@ -167,18 +168,18 @@ class BookingCreateParticipantType extends AbstractType
/** /**
* Adds all configured dynamic fields to the form with state conditions applied. * 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 $dynamicFields = ['assignedRoomId']; // List of fields that need dynamic configuration
foreach ($dynamicFields as $fieldName) { foreach ($dynamicFields as $fieldName) {
if ($this->fieldOptionsProvider->hasFieldOptions($fieldName)) { if ($this->fieldOptionsProvider->hasFieldOptions($fieldName)) {
// Get base field options // 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 // Apply dynamic field state if conditions exist
if ($this->fieldOptionsProvider->hasStateConditions($fieldName)) { if ($this->fieldOptionsProvider->hasStateConditions($fieldName)) {
$fieldState = $this->fieldOptionsProvider->getFieldState($fieldName, $bookingCreateDto, $participantIndex); $fieldState = $this->fieldOptionsProvider->getFieldState($fieldName, $bookingDto, $participantIndex);
$fieldOptions = $this->mergeFieldState($fieldOptions, $fieldState); $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\Pickup;
use App\BusProNet\Model\Service; use App\BusProNet\Model\Service;
use App\Form\Model\ParticipantDto; use App\Form\Model\ParticipantDto;
use App\Form\Model\BookingDtoInterface;
use App\Form\Service\EditFieldStateProvider;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\BirthdayType; use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
@@ -19,6 +21,10 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class BookingEditParticipantType extends AbstractType class BookingEditParticipantType extends AbstractType
{ {
public function __construct(
private readonly EditFieldStateProvider $fieldStateProvider,
) {}
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) { $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
@@ -33,34 +39,33 @@ class BookingEditParticipantType extends AbstractType
$form = $event->getForm(); $form = $event->getForm();
$personalDataMutable = $options['personal_data_mutable'] && $participantData->mutable; // Traverse up the form tree to get the root form's data (BookingEditDto)
$isApplicant = 0 === $participantIndex; $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 $form
->add('firstName', TextType::class, [ ->add('firstName', TextType::class, $this->mergeFieldState([
'label' => 'Vorname', 'label' => 'Vorname',
'attr' => [
'readonly' => false === $personalDataMutable,
],
'clean_xss' => true, 'clean_xss' => true,
]) ], $getState('firstName')))
->add('lastName', TextType::class, [ ->add('lastName', TextType::class, $this->mergeFieldState([
'label' => 'Nachname', 'label' => 'Nachname',
'attr' => [
'readonly' => false === $personalDataMutable,
],
'clean_xss' => true, 'clean_xss' => true,
]) ], $getState('lastName')))
->add('dateOfBirth', BirthdayType::class, [ ->add('dateOfBirth', BirthdayType::class, $this->mergeFieldState([
'label' => 'Geburtsdatum', 'label' => 'Geburtsdatum',
'html5' => true, 'html5' => true,
'widget' => 'single_text', 'widget' => 'single_text',
'input' => 'datetime_immutable', 'input' => 'datetime_immutable',
'attr' => [ ], $getState('dateOfBirth')))
'readonly' => false === $personalDataMutable, ->add('gender', ChoiceType::class, $this->mergeFieldState([
],
])
->add('gender', ChoiceType::class, [
'label' => 'Geschlecht', 'label' => 'Geschlecht',
'required' => false, 'required' => false,
'placeholder' => 'keine Angabe', 'placeholder' => 'keine Angabe',
@@ -69,36 +74,22 @@ class BookingEditParticipantType extends AbstractType
'weiblich' => 'W', 'weiblich' => 'W',
'divers' => 'D', 'divers' => 'D',
], ],
'attr' => [ ], $getState('gender')))
'readonly' => false === $personalDataMutable, ->add('nationality', CountryType::class, $this->mergeFieldState([
'style' => false === $personalDataMutable ? 'pointer-events: none' : null,
],
])
->add('nationality', CountryType::class, [
'label' => 'Nationalität', 'label' => 'Nationalität',
'property' => 'nationality', 'property' => 'nationality',
'preferred_choices' => ['D', 'A', 'CH'], 'preferred_choices' => ['D', 'A', 'CH'],
'attr' => [ ], $getState('nationality')))
'readonly' => false === $personalDataMutable, ->add('email', EmailType::class, $this->mergeFieldState([
'style' => false === $personalDataMutable ? 'pointer-events: none' : null,
],
])
->add('email', EmailType::class, [
'label' => 'E-Mail', 'label' => 'E-Mail',
'required' => false, 'required' => false,
'attr' => [
'readonly' => true === $isApplicant,
],
'clean_xss' => true, 'clean_xss' => true,
]) ], $getState('email')))
->add('mobile', TextType::class, [ ->add('mobile', TextType::class, $this->mergeFieldState([
'label' => 'Telefon (mobil)', 'label' => 'Telefon (mobil)',
'required' => false, 'required' => false,
'attr' => [
'readonly' => true === $isApplicant,
],
'clean_xss' => true, 'clean_xss' => true,
]) ], $getState('mobile')))
->add('bodyDimensions', BodyDimensionsType::class); ->add('bodyDimensions', BodyDimensionsType::class);
$commonChoiceFieldOptions = [ $commonChoiceFieldOptions = [
@@ -294,6 +285,33 @@ class BookingEditParticipantType extends AbstractType
return; 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; $transportationId = $data['transportationServiceTo'] ?? null;
$transportation = $options['travel']->pickups[$transportationId] ?? null; $transportation = $options['travel']->pickups[$transportationId] ?? null;
@@ -344,6 +362,33 @@ class BookingEditParticipantType extends AbstractType
'transportation_services_mutable' => true, 'transportation_services_mutable' => true,
'pickups_mutable' => true, 'pickups_mutable' => true,
'applicant_id' => null, '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 * @return ParticipantDto|null The participant DTO or null if not found
*/ */
public function getParticipant(int $index): ?ParticipantDto; 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; 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; namespace App\Form\ParticipantFieldHandler;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingDtoInterface;
/** /**
* Abstract base class providing common functionality for participant field handlers. * 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 * if the participant exists at the given index. Returns null if the
* participant doesn't exist, preventing array access errors. * participant doesn't exist, preventing array access errors.
* *
* @param BookingCreateDto $bookingDto The booking DTO containing participants * @param BookingDtoInterface $bookingDto The booking DTO containing participants (create or edit)
* @param int $participantIndex The index of the participant to retrieve * @param int $participantIndex The index of the participant to retrieve
* *
* @return object|null The participant object, or null if not found * @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. * processing results.
* *
* @param array<string, mixed> $submittedData The submitted participant form data * @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 * @param int $participantIndex The participant index being processed
* *
* @return array<string, array<string, mixed>> Empty array (no state modifications by default) * @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 []; return [];
} }
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition; namespace App\Form\ParticipantFieldHandler\Condition;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingDtoInterface;
/** /**
* Condition that evaluates participant age against specified range criteria. * 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 * checks if it falls within the configured age range. Returns false if
* the participant has no date of birth set. * 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 int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data (unused for age conditions) * @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 * @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) { if (null === $participant || null === $participant->dateOfBirth) {
return false; 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; namespace App\Form\ParticipantFieldHandler\Condition;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingDtoInterface;
/** /**
* Composite condition that combines multiple conditions with logical operators. * 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 * evaluation for optimal performance. The evaluation stops as soon as the
* final result can be determined. * 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 int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation * @param array<string, mixed> $formData Current form data for condition evaluation
* *
* @return bool True if the composite condition is met, false otherwise * @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) { return match ($this->operator) {
self::OPERATOR_AND => $this->evaluateAnd($bookingDto, $participantIndex, $formData), 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, * Returns false as soon as any condition evaluates to false,
* avoiding unnecessary evaluation of remaining conditions. * 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) { foreach ($this->conditions as $condition) {
if (!$condition->evaluate($bookingDto, $participantIndex, $formData)) { if (!$condition->evaluate($bookingDto, $participantIndex, $formData)) {
@@ -184,7 +184,7 @@ class CompositeCondition implements FieldConditionInterface
* Returns true as soon as any condition evaluates to true, * Returns true as soon as any condition evaluates to true,
* avoiding unnecessary evaluation of remaining conditions. * 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) { foreach ($this->conditions as $condition) {
if ($condition->evaluate($bookingDto, $participantIndex, $formData)) { 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. * 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); return !$this->conditions[0]->evaluate($bookingDto, $participantIndex, $formData);
} }
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition; namespace App\Form\ParticipantFieldHandler\Condition;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/** /**
* Interface for evaluating field state conditions. * Interface for evaluating field state conditions.
@@ -29,13 +30,13 @@ interface FieldConditionInterface
* state of the booking, participant data, and submitted form values. * state of the booking, participant data, and submitted form values.
* The result is used to determine field state (enabled/disabled/readonly). * 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 int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data (may include partial submissions) * @param array<string, mixed> $formData Current form data (may include partial submissions)
* *
* @return bool True if the condition is met, false otherwise * @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. * Returns field names that trigger re-evaluation of this condition.
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition; 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. * 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 * against the expected value using the configured operator. Supports
* both participant-level fields and booking-level fields. * 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 int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation * @param array<string, mixed> $formData Current form data for condition evaluation
* *
* @return bool True if the field value meets the condition criteria, false otherwise * @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); $fieldValue = $this->getFieldValue($formData, $participantIndex, $bookingDto);
@@ -174,7 +174,7 @@ class FieldValueCondition implements FieldConditionInterface
/** /**
* Retrieves field value from form data or participant data. * 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 // First check participant-specific form data
if (isset($formData['participants'][$participantIndex][$this->fieldName])) { if (isset($formData['participants'][$participantIndex][$this->fieldName])) {
@@ -182,7 +182,7 @@ class FieldValueCondition implements FieldConditionInterface
} }
// Then check participant DTO data // Then check participant DTO data
$participant = $bookingDto->participants[$participantIndex] ?? null; $participant = $bookingDto->getParticipant($participantIndex);
if (null !== $participant && property_exists($participant, $this->fieldName)) { if (null !== $participant && property_exists($participant, $this->fieldName)) {
return $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; namespace App\Form\ParticipantFieldHandler;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/** /**
* Handles processing of the assignedRoomId field for booking participants. * Handles processing of the assignedRoomId field for booking participants.
@@ -52,10 +53,10 @@ class ParticipantAssignedRoomFieldHandler extends AbstractParticipantFieldHandle
* 4. Updates the participant's assignedRoomId property * 4. Updates the participant's assignedRoomId property
* *
* @param array<string, mixed> $submittedData The submitted participant form data * @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 * @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 // Safely get the participant object, returning early if not found
$participant = $this->getParticipant($bookingDto, $participantIndex); $participant = $this->getParticipant($bookingDto, $participantIndex);
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler; 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. * 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. * Processes the participant field data from submitted form data and updates the DTO.
* *
* @param array<string, mixed> $submittedData The submitted participant form data * @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 * @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. * 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. * to enable/disable/hide fields based on the handler's processing results.
* *
* @param array<string, mixed> $submittedData The submitted participant form data * @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 * @param int $participantIndex The participant index being processed
* *
* @return array<string, array<string, mixed>> Field state modifications indexed by field name * @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. * Returns field names whose state is affected by this handler's processing.
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler; namespace App\Form\ParticipantFieldHandler;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
/** /**
* Registry for managing and executing participant field handlers in dependency order. * 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. * participant's data and updates the booking DTO accordingly.
* *
* @param array<string, mixed> $submittedData The submitted form data containing participants array * @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 // Early return if no participant data exists in submission
if (!isset($submittedData['participants']) || !is_array($submittedData['participants'])) { 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; namespace App\Form\Service;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingDtoInterface;
/** /**
* Interface for providing dynamic field state based on conditions. * Interface for providing dynamic field state based on conditions.
@@ -46,13 +46,13 @@ interface FieldStateProviderInterface
* - 'attr' => ['class' => 'conditional-field'] - Add CSS classes * - 'attr' => ['class' => 'conditional-field'] - Add CSS classes
* *
* @param string $fieldName The name of the field to evaluate * @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 int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data (may include partial submissions) * @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 * @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. * 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 * when multiple field states need to be determined simultaneously. It's
* particularly useful during form building and bulk state updates. * 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 int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation * @param array<string, mixed> $formData Current form data for condition evaluation
* *
* @return array<string, array<string, mixed>> Field states indexed by field name * @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; namespace App\Form\Service;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingDtoInterface;
use App\Form\ParticipantFieldHandler\Condition\FieldConditionInterface; use App\Form\ParticipantFieldHandler\Condition\FieldConditionInterface;
/** /**
@@ -64,13 +64,13 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
* - 'disabled' - Whether the field should be disabled * - 'disabled' - Whether the field should be disabled
* - 'required' - Whether the field is required * - 'required' - Whether the field is required
* *
* @param string $fieldName The name of the field to configure * @param string $fieldName The name of the field to configure
* @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 configured * @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 * @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 // Check if we have a provider for this field
if (!isset($this->fieldOptionProviders[$fieldName])) { if (!isset($this->fieldOptionProviders[$fieldName])) {
@@ -121,7 +121,7 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
private function registerFieldOptionProviders(): void private function registerFieldOptionProviders(): void
{ {
// Room assignment field provider // Room assignment field provider
$this->fieldOptionProviders['assignedRoomId'] = fn (BookingCreateDto $bookingDto, int $participantIndex) => [ $this->fieldOptionProviders['assignedRoomId'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
'label' => 'Zimmer', 'label' => 'Zimmer',
'placeholder' => 'Bitte wählen', 'placeholder' => 'Bitte wählen',
// Use factory to create context-aware choice loader that: // Use factory to create context-aware choice loader that:
@@ -155,13 +155,13 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
* disabled, etc.) and evaluated independently. * disabled, etc.) and evaluated independently.
* *
* @param string $fieldName The name of the field to evaluate * @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 int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation * @param array<string, mixed> $formData Current form data for condition evaluation
* *
* @return array<string, mixed> Symfony form field options for state modifications * @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])) { if (!isset($this->fieldStateConditions[$fieldName])) {
return []; return [];
@@ -233,13 +233,13 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
/** /**
* Calculates field states for all configured fields at once. * 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 int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation * @param array<string, mixed> $formData Current form data for condition evaluation
* *
* @return array<string, array<string, mixed>> Field states indexed by field name * @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 = []; $allStates = [];
+3 -3
View File
@@ -16,9 +16,9 @@
</div> </div>
<div> <div>
<h3>Zusammenfassung</h3> <h3>Zusammenfassung</h3>
<p>Reise: {{ bookingCreateDto.travelData.label }}</p> <p>Reise: {{ bookingCreateDto.travel.label }}</p>
<p>Datum: {{ bookingCreateDto.travelData.dateFrom | date('d.m.Y') }} - {{ bookingCreateDto.travelData.dateTo | date('d.m.Y') }}</p> <p>Datum: {{ bookingCreateDto.travel.dateFrom | date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo | date('d.m.Y') }}</p>
<p>Hotel: {{ bookingCreateDto.travelData.hotel.name }}</p> <p>Hotel: {{ bookingCreateDto.travel.hotel.name }}</p>
{% if participantsCount > 0 %} {% if participantsCount > 0 %}
<p>Teilnehmerzahl: {{ participantsCount }}</p> <p>Teilnehmerzahl: {{ participantsCount }}</p>
{% endif %} {% endif %}
+3 -3
View File
@@ -71,19 +71,19 @@
Reise Reise
</h4> </h4>
<p> <p>
{{ bookingCreateDto.travelData.label }} {{ bookingCreateDto.travel.label }}
</p> </p>
<h4> <h4>
Datum Datum
</h4> </h4>
<p> <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> </p>
<h4> <h4>
Unterkunft Unterkunft
</h4> </h4>
<p> <p>
{{ bookingCreateDto.travelData.hotel.name }} {{ bookingCreateDto.travel.hotel.name }}
</p> </p>
{% if participantsCount > 0 %} {% if participantsCount > 0 %}
<h4> <h4>