wip: refactoring
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Form;
|
||||
|
||||
use App\BusProNet\Form\CountryType;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Service\ParticipantRoomChoiceLoaderFactory;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
@@ -27,9 +28,11 @@ class BookingCreateParticipantType extends AbstractType
|
||||
$builder
|
||||
->add('firstName', TextType::class, [
|
||||
'label' => 'Vorname',
|
||||
'clean_xss' => true,
|
||||
])
|
||||
->add('lastName', TextType::class, [
|
||||
'label' => 'Nachname',
|
||||
'clean_xss' => true,
|
||||
])
|
||||
->add('dateOfBirth', BirthdayType::class, [
|
||||
'label' => 'Geburtsdatum',
|
||||
@@ -55,19 +58,36 @@ class BookingCreateParticipantType extends AbstractType
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'E-Mail',
|
||||
'required' => false,
|
||||
'clean_xss' => true,
|
||||
])
|
||||
->add('mobile', TextType::class, [
|
||||
'label' => 'Telefon (mobil)',
|
||||
'required' => false,
|
||||
'clean_xss' => true,
|
||||
])
|
||||
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
|
||||
$data = $event->getData();
|
||||
/** @var ParticipantDto|null $participantData */
|
||||
$participantData = $event->getData();
|
||||
$form = $event->getForm();
|
||||
|
||||
$choiceLoader = $this->choiceLoaderFactory->createChoiceLoader(
|
||||
$options['participant_adapter'],
|
||||
if (null === $participantData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse up the form tree to get the root form's data.
|
||||
$rootForm = $form;
|
||||
while ($rootForm->getParent()) {
|
||||
$rootForm = $rootForm->getParent();
|
||||
}
|
||||
|
||||
/** @var BookingCreateDto $bookingCreateDto */
|
||||
$bookingCreateDto = $rootForm->getData();
|
||||
$allParticipants = $bookingCreateDto->participants;
|
||||
|
||||
$choiceLoader = $this->choiceLoaderFactory->create(
|
||||
$allParticipants,
|
||||
$options['selected_rooms'],
|
||||
$data->index
|
||||
$participantData->index
|
||||
);
|
||||
|
||||
$form->add('assignedRoomId', ChoiceType::class, [
|
||||
@@ -83,8 +103,9 @@ class BookingCreateParticipantType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ParticipantDto::class,
|
||||
'participant_adapter' => null,
|
||||
'selected_rooms' => [],
|
||||
]);
|
||||
|
||||
$resolver->setAllowedTypes('selected_rooms', 'array');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,61 +2,37 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Form\DataAdapters\ParticipantDataAdapter;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class BookingCreateStep2Type extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, [$this, 'onPreSetData']);
|
||||
$builder->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'onPreSubmit']);
|
||||
}
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
|
||||
/** @var BookingCreateDto|null $data */
|
||||
$data = $event->getData();
|
||||
$form = $event->getForm();
|
||||
|
||||
public function onPreSetData(FormEvent $event): void
|
||||
{
|
||||
/** @var BookingCreateDto $data */
|
||||
$data = $event->getData();
|
||||
$form = $event->getForm();
|
||||
if (null === $data) {
|
||||
return;
|
||||
}
|
||||
|
||||
$adapter = new ParticipantDataAdapter($data->participants);
|
||||
$this->addParticipantsField($form, $adapter, $data->getSelectedRooms());
|
||||
}
|
||||
|
||||
public function onPreSubmit(FormEvent $event): void
|
||||
{
|
||||
$data = $event->getData();
|
||||
$form = $event->getForm();
|
||||
/** @var BookingCreateDto $bookingCreateDto */
|
||||
$bookingCreateDto = $form->getData();
|
||||
|
||||
$adapter = new ParticipantDataAdapter($data['participants']);
|
||||
$form->remove('participants');
|
||||
$this->addParticipantsField($form, $adapter, $bookingCreateDto->getSelectedRooms());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the participants collection field to the form.
|
||||
*/
|
||||
private function addParticipantsField(FormInterface $form, ParticipantDataAdapter $adapter, array $selectedRooms): void
|
||||
{
|
||||
$form->add('participants', CollectionType::class, [
|
||||
'entry_type' => BookingCreateParticipantType::class,
|
||||
'allow_add' => false,
|
||||
'allow_delete' => false,
|
||||
'by_reference' => false,
|
||||
'entry_options' => [
|
||||
'participant_adapter' => $adapter,
|
||||
'selected_rooms' => $selectedRooms,
|
||||
],
|
||||
]);
|
||||
$form->add('participants', CollectionType::class, [
|
||||
'entry_type' => BookingCreateParticipantType::class,
|
||||
'allow_add' => false,
|
||||
'allow_delete' => false,
|
||||
'by_reference' => false,
|
||||
'entry_options' => [
|
||||
'selected_rooms' => $data->getSelectedRooms(),
|
||||
],
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
|
||||
@@ -42,12 +42,14 @@ class BookingEditParticipantType extends AbstractType
|
||||
'attr' => [
|
||||
'readonly' => false === $personalDataMutable,
|
||||
],
|
||||
'anti_xss' => true,
|
||||
])
|
||||
->add('lastName', TextType::class, [
|
||||
'label' => 'Nachname',
|
||||
'attr' => [
|
||||
'readonly' => false === $personalDataMutable,
|
||||
],
|
||||
'anti_xss' => true,
|
||||
])
|
||||
->add('dateOfBirth', BirthdayType::class, [
|
||||
'label' => 'Geburtsdatum',
|
||||
@@ -87,6 +89,7 @@ class BookingEditParticipantType extends AbstractType
|
||||
'attr' => [
|
||||
'readonly' => true === $isApplicant,
|
||||
],
|
||||
'anti_xss' => true,
|
||||
])
|
||||
->add('mobile', TextType::class, [
|
||||
'label' => 'Telefon (mobil)',
|
||||
@@ -94,6 +97,7 @@ class BookingEditParticipantType extends AbstractType
|
||||
'attr' => [
|
||||
'readonly' => true === $isApplicant,
|
||||
],
|
||||
'anti_xss' => true,
|
||||
])
|
||||
->add('height', ChoiceType::class, [
|
||||
'label' => 'Körpergröße',
|
||||
@@ -379,7 +383,6 @@ class BookingEditParticipantType extends AbstractType
|
||||
'transportation_services_mutable' => true,
|
||||
'pickups_mutable' => true,
|
||||
'applicant_id' => null,
|
||||
'anti_xss' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
final class BookingEditType extends AbstractType
|
||||
class BookingEditType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
@@ -69,7 +69,6 @@ final class BookingEditType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => BookingEditDto::class,
|
||||
'anti_xss' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\ChoiceLoader;
|
||||
|
||||
use App\Form\DataAdapters\ParticipantDataAdapter;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Model\RoomSelectionDto;
|
||||
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
|
||||
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
|
||||
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
|
||||
@@ -21,9 +22,14 @@ class ParticipantRoomChoiceLoader implements ChoiceLoaderInterface
|
||||
{
|
||||
private ?ChoiceListInterface $choiceList = null;
|
||||
|
||||
/**
|
||||
* @param ParticipantDto[] $allParticipants all participant DTOs from the root form
|
||||
* @param RoomSelectionDto[] $selectedRooms the rooms selected in the previous step
|
||||
* @param int $participantIndex the index of the current participant
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ChoiceListFactoryInterface $factory,
|
||||
private readonly ParticipantDataAdapter $adapter,
|
||||
private readonly array $allParticipants,
|
||||
private readonly array $selectedRooms,
|
||||
private readonly int $participantIndex,
|
||||
) {
|
||||
@@ -66,7 +72,7 @@ class ParticipantRoomChoiceLoader implements ChoiceLoaderInterface
|
||||
{
|
||||
$roomOccupancy = $this->calculateRoomOccupancy();
|
||||
$participantRoomChoices = [];
|
||||
$assignedRoomId = $this->getParticipantAssignedRoomId();
|
||||
$assignedRoomId = $this->allParticipants[$this->participantIndex]->assignedRoomId ?? null;
|
||||
|
||||
foreach ($this->selectedRooms as $roomSelection) {
|
||||
$currentOccupancy = $roomOccupancy[$roomSelection->roomId] ?? 0;
|
||||
@@ -77,7 +83,8 @@ class ParticipantRoomChoiceLoader implements ChoiceLoaderInterface
|
||||
--$adjustedOccupancy;
|
||||
}
|
||||
|
||||
$remainingCapacity = $roomSelection->minPax - $adjustedOccupancy;
|
||||
$totalAvailableRooms = $roomSelection->quantity;
|
||||
$remainingCapacity = $totalAvailableRooms - $adjustedOccupancy;
|
||||
|
||||
// Include room if it has capacity OR if it's the participant's current assignment
|
||||
if ($remainingCapacity > 0 || $assignedRoomId === $roomSelection->roomId) {
|
||||
@@ -89,7 +96,7 @@ class ParticipantRoomChoiceLoader implements ChoiceLoaderInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates room occupancy based on participant assignments.
|
||||
* Calculates room occupancy based on all participant assignments.
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
@@ -97,24 +104,12 @@ class ParticipantRoomChoiceLoader implements ChoiceLoaderInterface
|
||||
{
|
||||
$roomOccupancy = [];
|
||||
|
||||
foreach ($this->adapter->getParticipants() as $participant) {
|
||||
$assignedRoomId = $this->adapter->getAssignedRoomId($participant);
|
||||
if (null !== $assignedRoomId) {
|
||||
$roomOccupancy[$assignedRoomId] = ($roomOccupancy[$assignedRoomId] ?? 0) + 1;
|
||||
foreach ($this->allParticipants as $participant) {
|
||||
if (null !== $participant->assignedRoomId) {
|
||||
$roomOccupancy[$participant->assignedRoomId] = ($roomOccupancy[$participant->assignedRoomId] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $roomOccupancy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the assigned room ID for the current participant.
|
||||
*/
|
||||
private function getParticipantAssignedRoomId(): ?int
|
||||
{
|
||||
$participants = $this->adapter->getParticipants();
|
||||
$participant = $participants[$this->participantIndex] ?? null;
|
||||
|
||||
return null !== $participant ? $this->adapter->getAssignedRoomId($participant) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\DataAdapters;
|
||||
|
||||
/**
|
||||
* Adapter pattern to unify access to participant data regardless of whether it comes from
|
||||
* objects (during PRE_SET_DATA) or arrays (during PRE_SUBMIT).
|
||||
*/
|
||||
class ParticipantDataAdapter
|
||||
{
|
||||
private array $participants;
|
||||
|
||||
public function __construct(array $participants)
|
||||
{
|
||||
$this->participants = $participants;
|
||||
}
|
||||
|
||||
public function getParticipants(): array
|
||||
{
|
||||
return $this->participants;
|
||||
}
|
||||
|
||||
public function getAssignedRoomId(mixed $participant): ?int
|
||||
{
|
||||
if (is_array($participant)) {
|
||||
$assignedRoomId = $participant['assignedRoomId'] ?? null;
|
||||
return null !== $assignedRoomId ? (int) $assignedRoomId : null;
|
||||
}
|
||||
|
||||
return $participant->assignedRoomId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form\DataTransformer;
|
||||
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use voku\helper\AntiXSS;
|
||||
|
||||
class XssCleanTransformer implements DataTransformerInterface
|
||||
{
|
||||
private ?AntiXSS $antiXss = null;
|
||||
|
||||
public function transform(mixed $value): mixed
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function reverseTransform(mixed $value): mixed
|
||||
{
|
||||
if (false === is_string($value) || true === empty($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (null === $this->antiXss) {
|
||||
$this->antiXss = new AntiXSS();
|
||||
}
|
||||
|
||||
return $this->antiXss->xss_clean($value);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form\Extension;
|
||||
|
||||
use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use voku\helper\AntiXSS;
|
||||
|
||||
class AntiXssExtension extends AbstractTypeExtension
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
if (false === $options['anti_xss']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
|
||||
$data = $event->getData();
|
||||
if (null === $data) {
|
||||
return;
|
||||
}
|
||||
$antiXss = new AntiXSS();
|
||||
foreach ($data as $key => $value) {
|
||||
$data[$key] = $antiXss->xss_clean($value);
|
||||
}
|
||||
$event->setData($data);
|
||||
});
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'anti_xss' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getExtendedTypes(): iterable
|
||||
{
|
||||
return [FormType::class];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form\Extension;
|
||||
|
||||
use App\Form\DataTransformer\XssCleanTransformer;
|
||||
use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class XssCleanExtension extends AbstractTypeExtension
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
if (true === $options['clean_xss']) {
|
||||
$builder->addModelTransformer(new XssCleanTransformer());
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'clean_xss' => false,
|
||||
]);
|
||||
$resolver->setAllowedTypes('clean_xss', 'bool');
|
||||
}
|
||||
|
||||
public static function getExtendedTypes(): iterable
|
||||
{
|
||||
return [
|
||||
TextType::class,
|
||||
TextareaType::class,
|
||||
EmailType::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class PersonalDataType extends AbstractType
|
||||
{
|
||||
@@ -17,14 +16,17 @@ class PersonalDataType extends AbstractType
|
||||
->add('street', TextType::class, [
|
||||
'label' => 'Straße',
|
||||
'property_path' => 'address.street',
|
||||
'anti_xss' => true,
|
||||
])
|
||||
->add('postCode', TextType::class, [
|
||||
'label' => 'PLZ',
|
||||
'property_path' => 'address.postCode',
|
||||
'anti_xss' => true,
|
||||
])
|
||||
->add('city', TextType::class, [
|
||||
'label' => 'Stadt',
|
||||
'property_path' => 'address.city',
|
||||
'anti_xss' => true,
|
||||
])
|
||||
->add('country', CountryType::class, [
|
||||
'label' => 'Land',
|
||||
@@ -33,23 +35,19 @@ class PersonalDataType extends AbstractType
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'E-Mail',
|
||||
'property_path' => 'communication.email',
|
||||
'anti_xss' => true,
|
||||
])
|
||||
->add('phone', TextType::class, [
|
||||
'label' => 'Telefon',
|
||||
'required' => false,
|
||||
'property_path' => 'communication.phone',
|
||||
'anti_xss' => true,
|
||||
])
|
||||
->add('mobile', TextType::class, [
|
||||
'label' => 'Mobil',
|
||||
'property_path' => 'communication.mobile',
|
||||
'anti_xss' => true,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'anti_xss' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,14 @@ class RegistrationType extends AbstractType
|
||||
])
|
||||
->add('firstName', TextType::class, [
|
||||
'label' => 'Name',
|
||||
'anti_xss' => true,
|
||||
])
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'Nachname',
|
||||
])
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'E-Mail',
|
||||
'property_path' => 'email',
|
||||
'anti_xss' => true,
|
||||
])
|
||||
;
|
||||
}
|
||||
@@ -40,7 +41,6 @@ class RegistrationType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => RegistrationDto::class,
|
||||
'anti_xss' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,12 @@ declare(strict_types=1);
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\Form\ChoiceLoader\ParticipantRoomChoiceLoader;
|
||||
use App\Form\DataAdapters\ParticipantDataAdapter;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Model\RoomSelectionDto;
|
||||
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
|
||||
|
||||
/**
|
||||
* Factory service for creating ParticipantRoomChoiceLoader instances.
|
||||
*
|
||||
* This service provides a clean way to create choice loaders with proper
|
||||
* dependency injection instead of manually instantiating factories in form types.
|
||||
*/
|
||||
class ParticipantRoomChoiceLoaderFactory
|
||||
{
|
||||
@@ -23,17 +21,20 @@ class ParticipantRoomChoiceLoaderFactory
|
||||
|
||||
/**
|
||||
* Creates a ParticipantRoomChoiceLoader for the given participant and room data.
|
||||
*
|
||||
* @param ParticipantDto[] $allParticipants
|
||||
* @param RoomSelectionDto[] $selectedRooms
|
||||
*/
|
||||
public function createChoiceLoader(
|
||||
ParticipantDataAdapter $adapter,
|
||||
public function create(
|
||||
array $allParticipants,
|
||||
array $selectedRooms,
|
||||
int $participantIndex,
|
||||
): ParticipantRoomChoiceLoader {
|
||||
return new ParticipantRoomChoiceLoader(
|
||||
$this->choiceListFactory,
|
||||
$adapter,
|
||||
$allParticipants,
|
||||
$selectedRooms,
|
||||
$participantIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user