feat: replace dropdowns with individual fields for body dimensions
This commit is contained in:
@@ -6,7 +6,7 @@ namespace App\Form;
|
||||
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -15,35 +15,38 @@ class BodyDimensionsType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$shoeSizeChoices = array_combine(
|
||||
range($options['shoe_size_min'], $options['shoe_size_max']),
|
||||
range($options['shoe_size_min'], $options['shoe_size_max'])
|
||||
);
|
||||
$ranges = $options['body_dimension_ranges'];
|
||||
|
||||
$builder
|
||||
->add('height', ChoiceType::class, [
|
||||
'label' => 'Körpergröße',
|
||||
->add('height', IntegerType::class, [
|
||||
'label' => 'Körpergröße (cm)',
|
||||
'required' => $options['height_required'],
|
||||
'expanded' => false,
|
||||
'multiple' => false,
|
||||
'placeholder' => 'Keine Angabe',
|
||||
'choices' => $options['height_choices'],
|
||||
'empty_data' => null,
|
||||
'attr' => [
|
||||
'placeholder' => sprintf('%d - %d', $ranges['height_min'], $ranges['height_max']),
|
||||
'min' => $ranges['height_min'],
|
||||
'max' => $ranges['height_max'],
|
||||
],
|
||||
])
|
||||
->add('shoeSize', ChoiceType::class, [
|
||||
->add('shoeSize', IntegerType::class, [
|
||||
'label' => 'Schuhgröße',
|
||||
'required' => $options['shoeSize_required'],
|
||||
'expanded' => false,
|
||||
'multiple' => false,
|
||||
'placeholder' => 'Keine Angabe',
|
||||
'choices' => $shoeSizeChoices,
|
||||
'empty_data' => null,
|
||||
'attr' => [
|
||||
'placeholder' => sprintf('%d - %d', $ranges['shoe_size_min'], $ranges['shoe_size_max']),
|
||||
'min' => $ranges['shoe_size_min'],
|
||||
'max' => $ranges['shoe_size_max'],
|
||||
],
|
||||
])
|
||||
->add('weight', ChoiceType::class, [
|
||||
'label' => 'Gewicht',
|
||||
->add('weight', IntegerType::class, [
|
||||
'label' => 'Gewicht (kg)',
|
||||
'required' => $options['weight_required'],
|
||||
'expanded' => false,
|
||||
'multiple' => false,
|
||||
'placeholder' => 'Keine Angabe',
|
||||
'choices' => $options['weight_choices'],
|
||||
'empty_data' => null,
|
||||
'attr' => [
|
||||
'placeholder' => sprintf('%d - %d', $ranges['weight_min'], $ranges['weight_max']),
|
||||
'min' => $ranges['weight_min'],
|
||||
'max' => $ranges['weight_max'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -51,22 +54,20 @@ class BodyDimensionsType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ParticipantDto::class,
|
||||
'height_required' => true,
|
||||
'weight_required' => true,
|
||||
'shoeSize_required' => true,
|
||||
'height_choices' => [],
|
||||
'weight_choices' => [],
|
||||
'shoe_size_min' => 36,
|
||||
'shoe_size_max' => 48,
|
||||
'help' => 'Verleih kann bis 4 Tage vor Anreise in MyE&P nachgebucht werden, sollten dir (noch) nicht alle Angaben vorliegen',
|
||||
'height_required' => false,
|
||||
'weight_required' => false,
|
||||
'shoeSize_required' => false,
|
||||
'help' => 'Du kannst die Daten auch später nachreichen',
|
||||
'help_attr' => [
|
||||
'class' => 'lg:col-span-3 text-sm -mt-2 px-2',
|
||||
],
|
||||
]);
|
||||
|
||||
$resolver->setRequired(['body_dimension_ranges']);
|
||||
|
||||
$resolver->setAllowedTypes('height_required', 'bool');
|
||||
$resolver->setAllowedTypes('weight_required', 'bool');
|
||||
$resolver->setAllowedTypes('shoeSize_required', 'bool');
|
||||
$resolver->setAllowedTypes('height_choices', 'array');
|
||||
$resolver->setAllowedTypes('weight_choices', 'array');
|
||||
$resolver->setAllowedTypes('shoe_size_min', 'int');
|
||||
$resolver->setAllowedTypes('shoe_size_max', 'int');
|
||||
$resolver->setAllowedTypes('body_dimension_ranges', 'array');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,12 +49,12 @@ class BookingParticipantType extends AbstractType
|
||||
: $this->createFieldStateProvider;
|
||||
|
||||
$builder
|
||||
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($bookingContext) {
|
||||
$this->onPreSetData($event, $bookingContext);
|
||||
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($bookingContext, $options) {
|
||||
$this->onPreSetData($event, $bookingContext, $options);
|
||||
})
|
||||
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($bookingContext) {
|
||||
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($bookingContext, $options) {
|
||||
$this->processFieldHandlers($event, $bookingContext);
|
||||
$this->onPreSubmit($event, $bookingContext);
|
||||
$this->onPreSubmit($event, $bookingContext, $options);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,7 +78,6 @@ class BookingParticipantType extends AbstractType
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var ParticipantEditDto $data */
|
||||
$data = $form->getData();
|
||||
|
||||
// Process all field handlers for this participant and sync submitted data
|
||||
@@ -94,23 +93,17 @@ class BookingParticipantType extends AbstractType
|
||||
|
||||
/**
|
||||
* Adds dynamic fields to the form based on participant data.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
private function onPreSetData(FormEvent $event, ?BookingDto $bookingContext): void
|
||||
private function onPreSetData(FormEvent $event, ?BookingDto $bookingContext, array $options): void
|
||||
{
|
||||
/** @var ParticipantEditDto|null $data */
|
||||
$data = $event->getData();
|
||||
|
||||
if (null === $data) {
|
||||
return;
|
||||
}
|
||||
|
||||
$form = $event->getForm();
|
||||
|
||||
// Use bookingContext from wrapper DTO or fallback to passed option
|
||||
$data = $event->getData();
|
||||
$bookingDto = $data->bookingContext;
|
||||
|
||||
// Add base fields with states applied
|
||||
$this->addBaseFields($form, $bookingDto, $data->participant->index);
|
||||
$this->addBaseFields($form, $bookingDto, $data->participant->index, $options);
|
||||
|
||||
// Add dynamic fields
|
||||
$this->addDynamicFields($form, $bookingDto, $data->participant->index);
|
||||
@@ -118,29 +111,30 @@ class BookingParticipantType extends AbstractType
|
||||
|
||||
/**
|
||||
* Handles form pre-submit events to update field states based on submitted data.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
private function onPreSubmit(FormEvent $event, ?BookingDto $bookingContext): void
|
||||
private function onPreSubmit(FormEvent $event, ?BookingDto $bookingContext, array $options): void
|
||||
{
|
||||
$submittedData = $event->getData();
|
||||
$form = $event->getForm();
|
||||
|
||||
/** @var ParticipantEditDto $data */
|
||||
$data = $form->getData();
|
||||
|
||||
// Use bookingContext from wrapper DTO or fallback to passed option
|
||||
$bookingDto = $data->bookingContext;
|
||||
|
||||
// Rebuild all fields with updated states based on submitted data
|
||||
$this->rebuildFieldsWithStates($form, $bookingDto, $data->participant->index, $submittedData);
|
||||
$this->rebuildFieldsWithStates($form, $bookingDto, $data->participant->index, $submittedData, $options);
|
||||
|
||||
$event->setData($submittedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds base fields to the form with field states applied.
|
||||
*
|
||||
* @param FormInterface<mixed> $form
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
/** @param FormInterface<mixed> $form */
|
||||
private function addBaseFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex): void
|
||||
private function addBaseFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array $options): void
|
||||
{
|
||||
// Get field states for base fields
|
||||
$allFieldStates = $this->fieldStateProvider->getAllFieldStates($bookingDto, $participantIndex);
|
||||
@@ -247,10 +241,7 @@ class BookingParticipantType extends AbstractType
|
||||
if ($this->fieldStateProvider->shouldIncludeField('bodyDimensions', $bookingDto, $participantIndex)) {
|
||||
$form->add('bodyDimensions', BodyDimensionsType::class, [
|
||||
'property_path' => 'participant',
|
||||
'height_choices' => $form->getConfig()->getOption('height_choices'),
|
||||
'weight_choices' => $form->getConfig()->getOption('weight_choices'),
|
||||
'shoe_size_min' => $form->getConfig()->getOption('shoe_size_min'),
|
||||
'shoe_size_max' => $form->getConfig()->getOption('shoe_size_max'),
|
||||
'body_dimension_ranges' => $options['body_dimension_ranges'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -261,15 +252,11 @@ class BookingParticipantType extends AbstractType
|
||||
* This method handles dynamic field exclusion during form submission when
|
||||
* field states change based on submitted data.
|
||||
*
|
||||
* @param FormInterface $form The form to modify
|
||||
* @param FormInterface<mixed> $form The form to modify
|
||||
* @param BookingDto $bookingDto The booking data for context
|
||||
* @param int $participantIndex The participant index
|
||||
* @param array<string, mixed> $submittedData Submitted form data for state calculation
|
||||
*/
|
||||
/**
|
||||
* @param FormInterface<mixed> $form
|
||||
* @param array<string, mixed> $submittedData
|
||||
*/
|
||||
private function removeExcludedFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array &$submittedData = []): void
|
||||
{
|
||||
foreach (ParticipantDto::DYNAMIC_FIELDS as $fieldName) {
|
||||
@@ -289,8 +276,9 @@ class BookingParticipantType extends AbstractType
|
||||
/**
|
||||
* @param FormInterface<mixed> $form
|
||||
* @param array<string, mixed> $submittedData
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
private function rebuildFieldsWithStates(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array &$submittedData): void
|
||||
private function rebuildFieldsWithStates(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array &$submittedData, array $options): void
|
||||
{
|
||||
// First, remove fields that should be excluded entirely
|
||||
$this->removeExcludedFields($form, $bookingDto, $participantIndex, $submittedData);
|
||||
@@ -304,7 +292,7 @@ class BookingParticipantType extends AbstractType
|
||||
}
|
||||
|
||||
// Re-add base fields with updated states
|
||||
$this->addBaseFields($form, $bookingDto, $participantIndex);
|
||||
$this->addBaseFields($form, $bookingDto, $participantIndex, $options);
|
||||
|
||||
// Rebuild dynamic fields
|
||||
foreach (ParticipantDto::DYNAMIC_FIELDS as $fieldName) {
|
||||
@@ -415,10 +403,6 @@ class BookingParticipantType extends AbstractType
|
||||
'data_class' => ParticipantEditDto::class,
|
||||
'selected_rooms' => [],
|
||||
'booking_context' => null,
|
||||
'height_choices' => [],
|
||||
'weight_choices' => [],
|
||||
'shoe_size_min' => 36,
|
||||
'shoe_size_max' => 48,
|
||||
'validation_groups' => function (FormInterface $form) {
|
||||
/** @var ParticipantEditDto $data */
|
||||
$data = $form->getData();
|
||||
@@ -443,11 +427,10 @@ class BookingParticipantType extends AbstractType
|
||||
},
|
||||
]);
|
||||
|
||||
$resolver->setRequired(['body_dimension_ranges']);
|
||||
|
||||
$resolver->setAllowedTypes('selected_rooms', 'array');
|
||||
$resolver->setAllowedTypes('booking_context', ['null', BookingDto::class]);
|
||||
$resolver->setAllowedTypes('height_choices', 'array');
|
||||
$resolver->setAllowedTypes('weight_choices', 'array');
|
||||
$resolver->setAllowedTypes('shoe_size_min', 'int');
|
||||
$resolver->setAllowedTypes('shoe_size_max', 'int');
|
||||
$resolver->setAllowedTypes('body_dimension_ranges', 'array');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,25 @@ use App\Exception\ParticipantNotFoundException;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Model\ParticipantEditDto;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Shared helpers for participant edit forms in create and edit booking flows.
|
||||
*/
|
||||
class ParticipantFormSupport
|
||||
{
|
||||
/**
|
||||
* @var array<string, int>
|
||||
*/
|
||||
private array $bodyDimensionRanges;
|
||||
|
||||
/**
|
||||
* @param array<string, int> $bodyDimensionRanges
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ParameterBagInterface $parameterBag,
|
||||
array $bodyDimensionRanges,
|
||||
) {
|
||||
$this->bodyDimensionRanges = $this->resolveOptions($bodyDimensionRanges);
|
||||
}
|
||||
|
||||
public function ensureParticipantExists(BookingDto $bookingDto, int $index): ParticipantDto
|
||||
@@ -46,10 +55,7 @@ class ParticipantFormSupport
|
||||
{
|
||||
$options = [
|
||||
'booking_context' => $bookingDto,
|
||||
'height_choices' => $this->parameterBag->get('body_dimensions.height_choices'),
|
||||
'weight_choices' => $this->parameterBag->get('body_dimensions.weight_choices'),
|
||||
'shoe_size_min' => $this->parameterBag->get('body_dimensions.shoe_size_min'),
|
||||
'shoe_size_max' => $this->parameterBag->get('body_dimensions.shoe_size_max'),
|
||||
'body_dimension_ranges' => $this->bodyDimensionRanges,
|
||||
];
|
||||
|
||||
if (true === $disableValidation) {
|
||||
@@ -77,4 +83,29 @@ class ParticipantFormSupport
|
||||
|
||||
return array_values($notifications);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $options
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private function resolveOptions(array $options): array
|
||||
{
|
||||
$optionsResolver = new OptionsResolver();
|
||||
$optionsResolver->setRequired([
|
||||
'height_min',
|
||||
'height_max',
|
||||
'weight_min',
|
||||
'weight_max',
|
||||
'shoe_size_min',
|
||||
'shoe_size_max',
|
||||
]);
|
||||
$optionsResolver->setAllowedTypes('height_min', ['int']);
|
||||
$optionsResolver->setAllowedTypes('height_max', ['int']);
|
||||
$optionsResolver->setAllowedTypes('weight_min', ['int']);
|
||||
$optionsResolver->setAllowedTypes('weight_max', ['int']);
|
||||
$optionsResolver->setAllowedTypes('shoe_size_min', ['int']);
|
||||
$optionsResolver->setAllowedTypes('shoe_size_max', ['int']);
|
||||
|
||||
return $optionsResolver->resolve($options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
@@ -12,10 +13,24 @@ use Symfony\Component\Validator\ConstraintValidator;
|
||||
* Validates cross-field constraints on participant data.
|
||||
*
|
||||
* Enforces business rules that require examining multiple fields together,
|
||||
* such as requiring a pickup location when bus transportation is selected.
|
||||
* such as requiring a pickup location when bus transportation is selected,
|
||||
* and validates body dimension ranges when values are provided.
|
||||
*/
|
||||
class ParticipantValidator extends ConstraintValidator
|
||||
{
|
||||
private const RANGE_MESSAGE = 'Bitte gib eine Zahl zwischen %d und %d ein. Falls du außerhalb dieses Bereichs bist, ruf gerne unser Kundenoffice an.';
|
||||
|
||||
/** @var array<string, int> */
|
||||
private array $bodyDimensionRanges;
|
||||
|
||||
/**
|
||||
* @param array<string, int> $bodyDimensionRanges
|
||||
*/
|
||||
public function __construct(array $bodyDimensionRanges = [])
|
||||
{
|
||||
$this->bodyDimensionRanges = $this->resolveBodyDimensionRanges($bodyDimensionRanges);
|
||||
}
|
||||
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
/** @var ParticipantDto $participant */
|
||||
@@ -24,6 +39,7 @@ class ParticipantValidator extends ConstraintValidator
|
||||
$this->assertPickupSelected($participant);
|
||||
$this->assertDropOffSelected($participant);
|
||||
$this->assertBodyDimensionsWhenRentalsSelected($participant);
|
||||
$this->assertBodyDimensionsInRange($participant);
|
||||
}
|
||||
|
||||
public function assertPickupSelected(ParticipantDto $participant): void
|
||||
@@ -93,4 +109,71 @@ class ParticipantValidator extends ConstraintValidator
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
private function assertBodyDimensionsInRange(ParticipantDto $participant): void
|
||||
{
|
||||
$this->assertInRange(
|
||||
$participant->height,
|
||||
'height',
|
||||
$this->bodyDimensionRanges['height_min'],
|
||||
$this->bodyDimensionRanges['height_max']
|
||||
);
|
||||
|
||||
$this->assertInRange(
|
||||
$participant->weight,
|
||||
'weight',
|
||||
$this->bodyDimensionRanges['weight_min'],
|
||||
$this->bodyDimensionRanges['weight_max']
|
||||
);
|
||||
|
||||
$this->assertInRange(
|
||||
$participant->shoeSize,
|
||||
'shoeSize',
|
||||
$this->bodyDimensionRanges['shoe_size_min'],
|
||||
$this->bodyDimensionRanges['shoe_size_max']
|
||||
);
|
||||
}
|
||||
|
||||
private function assertInRange(int|string|null $value, string $path, int $min, int $max): void
|
||||
{
|
||||
if (null === $value || '' === $value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$intValue = (int) $value;
|
||||
|
||||
if ($min > $intValue || $max < $intValue) {
|
||||
$this->context->buildViolation(sprintf(self::RANGE_MESSAGE, $min, $max))
|
||||
->atPath($path)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $options
|
||||
*
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private function resolveBodyDimensionRanges(array $options): array
|
||||
{
|
||||
$resolver = new OptionsResolver();
|
||||
|
||||
$resolver->setDefaults([
|
||||
'height_min' => 145,
|
||||
'height_max' => 200,
|
||||
'weight_min' => 40,
|
||||
'weight_max' => 120,
|
||||
'shoe_size_min' => 35,
|
||||
'shoe_size_max' => 50,
|
||||
]);
|
||||
|
||||
$resolver->setAllowedTypes('height_min', 'int');
|
||||
$resolver->setAllowedTypes('height_max', 'int');
|
||||
$resolver->setAllowedTypes('weight_min', 'int');
|
||||
$resolver->setAllowedTypes('weight_max', 'int');
|
||||
$resolver->setAllowedTypes('shoe_size_min', 'int');
|
||||
$resolver->setAllowedTypes('shoe_size_max', 'int');
|
||||
|
||||
return $resolver->resolve($options);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user