wip: refactoring

This commit is contained in:
Björn Fromme
2025-07-23 10:27:04 +02:00
parent 77afc0fe7b
commit c85c72863c
14 changed files with 165 additions and 179 deletions
Generated
+13 -12
View File
@@ -1758,16 +1758,16 @@
},
{
"name": "league/flysystem-bundle",
"version": "3.4.0",
"version": "3.5.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-bundle.git",
"reference": "6493f7f2ab49bc5817e4b064b9b971d93faabc12"
"reference": "bf5ab3c072c0def47872e28f69876548d8669657"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-bundle/zipball/6493f7f2ab49bc5817e4b064b9b971d93faabc12",
"reference": "6493f7f2ab49bc5817e4b064b9b971d93faabc12",
"url": "https://api.github.com/repos/thephpleague/flysystem-bundle/zipball/bf5ab3c072c0def47872e28f69876548d8669657",
"reference": "bf5ab3c072c0def47872e28f69876548d8669657",
"shasum": ""
},
"require": {
@@ -1791,6 +1791,7 @@
"league/flysystem-read-only": "^3.15",
"league/flysystem-sftp-v3": "^3.1",
"league/flysystem-webdav": "^3.29",
"platformcommunity/flysystem-bunnycdn": "^3.3",
"symfony/dotenv": "^5.4 || ^6.0 || ^7.0",
"symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0",
"symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0",
@@ -1822,9 +1823,9 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem-bundle/issues",
"source": "https://github.com/thephpleague/flysystem-bundle/tree/3.4.0"
"source": "https://github.com/thephpleague/flysystem-bundle/tree/3.5.0"
},
"time": "2025-01-23T18:07:31+00:00"
"time": "2025-06-22T12:43:01+00:00"
},
{
"name": "league/flysystem-local",
@@ -10425,16 +10426,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.82.2",
"version": "v3.84.0",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "684ed3ab41008a2a4848de8bde17eb168c596247"
"reference": "38dad0767bf2a9b516b976852200ae722fe984ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/684ed3ab41008a2a4848de8bde17eb168c596247",
"reference": "684ed3ab41008a2a4848de8bde17eb168c596247",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/38dad0767bf2a9b516b976852200ae722fe984ca",
"reference": "38dad0767bf2a9b516b976852200ae722fe984ca",
"shasum": ""
},
"require": {
@@ -10518,7 +10519,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.82.2"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.84.0"
},
"funding": [
{
@@ -10526,7 +10527,7 @@
"type": "github"
}
],
"time": "2025-07-08T21:13:15+00:00"
"time": "2025-07-15T18:21:57+00:00"
},
{
"name": "myclabs/deep-copy",
+4
View File
@@ -62,3 +62,7 @@ services:
$logger: '@monolog.logger.core'
$preferRemote: '%env(bool:APP_TRAVEL_PREFER_REMOTE)%'
$enableFallback: '%env(bool:APP_TRAVEL_ENABLE_FALLBACK)%'
App\Form\Service\ParticipantRoomChoiceLoaderFactory:
arguments:
$choiceListFactory: '@form.choice_list_factory.default'
+26 -5
View File
@@ -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');
}
}
+6 -30
View File
@@ -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']);
}
public function onPreSetData(FormEvent $event): void
{
/** @var BookingCreateDto $data */
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
/** @var BookingCreateDto|null $data */
$data = $event->getData();
$form = $event->getForm();
$adapter = new ParticipantDataAdapter($data->participants);
$this->addParticipantsField($form, $adapter, $data->getSelectedRooms());
if (null === $data) {
return;
}
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,
'selected_rooms' => $data->getSelectedRooms(),
],
]);
});
}
public function configureOptions(OptionsResolver $resolver): void
+4 -1
View File
@@ -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,
]);
}
}
+1 -2
View File
@@ -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);
}
}
-45
View File
@@ -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];
}
}
+38
View File
@@ -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,
];
}
}
+6 -8
View File
@@ -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,
]);
}
}
+2 -2
View File
@@ -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,15 +21,18 @@ 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
);