wip: major refactoring

This commit is contained in:
Björn Fromme
2026-03-16 11:59:09 +01:00
parent d719b17aec
commit 11825191cf
32 changed files with 692 additions and 466 deletions
+3 -4
View File
@@ -2,12 +2,11 @@
$finder = (new PhpCsFixer\Finder()) $finder = (new PhpCsFixer\Finder())
->in(__DIR__) ->in(__DIR__)
->exclude('var') ->exclude('var');
;
return (new PhpCsFixer\Config()) return (new PhpCsFixer\Config())
->setRules([ ->setRules([
'@Symfony' => true, '@Symfony' => true,
'@DoctrineAnnotation' => true,
]) ])
->setFinder($finder) ->setFinder($finder);
;
+32 -6
View File
@@ -1,18 +1,26 @@
import { Controller } from '@hotwired/stimulus' import { Controller } from '@hotwired/stimulus'
export default class extends Controller { export default class extends Controller {
static classes = ['closed']
static classes = [ 'closed' ] static targets = ['toggle', 'icon']
static targets = [ 'toggle', 'icon' ]
static values = { static values = {
open: { open: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
storageKey: {
type: String,
default: ''
}
} }
initialize() { initialize() {
this.target = this.hasToggleTarget ? this.toggleTarget : this.element this.target = this.hasToggleTarget ? this.toggleTarget : this.element
// Restore state from storage if storageKey is provided
if (this.hasStorageKey()) {
this.restoreState()
}
} }
toggle() { toggle() {
@@ -30,10 +38,28 @@ export default class extends Controller {
openValueChanged(open) { openValueChanged(open) {
this.target.classList.toggle(this.closedClass, false === open) this.target.classList.toggle(this.closedClass, false === open)
if (false === this.hasIconTarget) { if (this.hasIconTarget) {
return this.iconTarget.classList.toggle('rotate-90', true === open)
} }
this.iconTarget.classList.toggle('rotate-90', true === open) // Save state to storage if storageKey is provided
if (this.hasStorageKey()) {
this.saveState()
}
}
hasStorageKey() {
return this.storageKeyValue && this.storageKeyValue.trim() !== ''
}
saveState() {
sessionStorage.setItem(`toggle_${this.storageKeyValue}`, this.openValue.toString())
}
restoreState() {
const savedState = sessionStorage.getItem(`toggle_${this.storageKeyValue}`)
if (savedState !== null) {
this.openValue = savedState === 'true'
}
} }
} }
+3 -3
View File
@@ -63,15 +63,15 @@ services:
$preferRemote: '%env(bool:APP_TRAVEL_PREFER_REMOTE)%' $preferRemote: '%env(bool:APP_TRAVEL_PREFER_REMOTE)%'
$enableFallback: '%env(bool:APP_TRAVEL_ENABLE_FALLBACK)%' $enableFallback: '%env(bool:APP_TRAVEL_ENABLE_FALLBACK)%'
App\Form\Service\ParticipantRoomChoiceLoaderFactory: App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory:
arguments: arguments:
$choiceListFactory: '@form.choice_list_factory.default' $choiceListFactory: '@form.choice_list_factory.default'
# Participant Field Handler Registry with hybrid handler configuration # Participant Field Handler Registry with hybrid handler configuration
App\Form\ParticipantFieldHandler\ParticipantFieldHandlerRegistry: App\Form\Service\ParticipantFieldHandlerRegistry:
arguments: arguments:
$handlers: $handlers:
# Simple handlers (no dependencies) - use class names # Simple handlers (no dependencies) - use class names
- 'App\Form\ParticipantFieldHandler\ParticipantAssignedRoomFieldHandler' - 'App\Form\Service\ParticipantAssignedRoomFieldHandler'
# Complex handlers (with dependencies) would use service references like: # Complex handlers (with dependencies) would use service references like:
# - '@participant.complex.handler' # - '@participant.complex.handler'
+89 -9
View File
@@ -4,14 +4,29 @@ This system provides a flexible architecture for implementing conditional field
## Architecture Overview ## Architecture Overview
The system consists of several key components: The system consists of several key components organized in a clean namespace structure:
1. **FieldConditionInterface** - Defines the contract for condition evaluation ### Core Interfaces (`src/Form/Service/Contract/`)
2. **Concrete Conditions** - Implement specific business logic (age ranges, field values, etc.) 1. **FieldStateProviderInterface** - Defines the contract for field state management
3. **CompositeCondition** - Combines conditions with AND/OR/NOT logic 2. **FieldOptionsProviderInterface** - Defines the contract for field option generation
4. **FieldStateProviderInterface** - Manages field state calculation
5. **ParticipantFieldOptionsProvider** - Enhanced to support state conditions ### Abstract Base Classes (`src/Form/Service/Abstract/`)
6. **Form Integration** - Applied in BookingCreateParticipantType 3. **AbstractFieldStateProvider** - Common field state functionality
4. **AbstractFieldOptionsProvider** - Common field option functionality
### Concrete Implementations (`src/Form/Service/`)
5. **CreateFieldStateProvider** - Field states for booking creation workflow
6. **EditFieldStateProvider** - Field states for booking edit workflow
7. **ParticipantFieldOptionsProvider** - Dynamic field option generation
### Condition System (`src/Form/Service/Condition/`)
8. **FieldConditionInterface** - Defines the contract for condition evaluation
9. **Concrete Conditions** - Implement specific business logic (age ranges, field values, etc.)
10. **CompositeCondition** - Combines conditions with AND/OR/NOT logic
### Form Integration
11. **BookingCreateParticipantType** - Uses CreateFieldStateProvider
12. **BookingEditParticipantType** - Uses EditFieldStateProvider
## Usage Examples ## Usage Examples
@@ -87,10 +102,12 @@ $this->fieldStateConditions['specialServices'] = [
## Adding New Conditions ## Adding New Conditions
To register field state conditions, add them to the `registerFieldStateConditions()` method in `ParticipantFieldOptionsProvider`: ### For Create Workflow
To register field state conditions for the booking creation workflow, add them to the `registerFieldStateConditions()` method in `CreateFieldStateProvider`:
```php ```php
private function registerFieldStateConditions(): void // src/Form/Service/CreateFieldStateProvider.php
protected function registerFieldStateConditions(): void
{ {
// Age-based readonly state // Age-based readonly state
$this->fieldStateConditions['assignedRoomId'] = [ $this->fieldStateConditions['assignedRoomId'] = [
@@ -113,6 +130,40 @@ private function registerFieldStateConditions(): void
} }
``` ```
### For Edit Workflow
To register field state conditions for the booking edit workflow, add them to the `registerFieldStateConditions()` method in `EditFieldStateProvider`:
```php
// src/Form/Service/EditFieldStateProvider.php
protected function registerFieldStateConditions(): void
{
// Make personal data readonly for applicants or non-mutable fields
$personalDataFields = ['firstName', 'lastName', 'dateOfBirth', 'gender', 'nationality', 'email', 'mobile'];
foreach ($personalDataFields as $field) {
$this->fieldStateConditions[$field] = [
'readonly' => CompositeCondition::or(
new ApplicantCondition(),
new MutabilityCondition()
),
];
}
}
```
### Adding Field Options
To register dynamic field options, add them to the `registerFieldOptionProviders()` method in `ParticipantFieldOptionsProvider`:
```php
// src/Form/Service/ParticipantFieldOptionsProvider.php
protected function registerFieldOptionProviders(): void
{
$this->fieldOptionProviders['newField'] = fn(BookingDtoInterface $bookingDto, int $participantIndex) => [
'label' => 'New Field Label',
'choices' => $this->generateChoicesFor($bookingDto, $participantIndex),
];
}
```
## Performance Considerations ## Performance Considerations
- Conditions use lazy evaluation and short-circuit logic - Conditions use lazy evaluation and short-circuit logic
@@ -136,6 +187,9 @@ The system supports real-time field state updates:
Create new condition classes implementing `FieldConditionInterface`: Create new condition classes implementing `FieldConditionInterface`:
```php ```php
// src/Form/Service/Condition/CustomBusinessRuleCondition.php
use App\Form\Service\Contract\FieldConditionInterface;
class CustomBusinessRuleCondition implements FieldConditionInterface class CustomBusinessRuleCondition implements FieldConditionInterface
{ {
public function evaluate(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool public function evaluate(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool
@@ -156,4 +210,30 @@ class CustomBusinessRuleCondition implements FieldConditionInterface
} }
``` ```
### Custom Field Options Providers
To create more complex field option logic, extend `AbstractFieldOptionsProvider`:
```php
// src/Form/Service/CustomFieldOptionsProvider.php
use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
class CustomFieldOptionsProvider extends AbstractFieldOptionsProvider
{
protected function registerFieldOptionProviders(): void
{
$this->fieldOptionProviders['customField'] = fn(BookingDtoInterface $bookingDto, int $participantIndex) => [
'label' => 'Custom Field',
'choices' => $this->generateCustomChoices($bookingDto, $participantIndex),
];
}
private function generateCustomChoices(BookingDtoInterface $bookingDto, int $participantIndex): array
{
// Custom choice generation logic
return [];
}
}
```
This system provides a powerful, maintainable foundation for complex conditional field behavior while maintaining clean separation of concerns and extensibility. This system provides a powerful, maintainable foundation for complex conditional field behavior while maintaining clean separation of concerns and extensibility.
@@ -34,8 +34,8 @@ class CreateStep1Controller extends AbstractController
{ {
$bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request); $bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request);
// Capture the current room selection state before form processing // Get or create baseline snapshot for change detection
$oldRoomSelectionSnapshot = $this->bookingService->createRoomSelectionSnapshot($bookingCreateDto); $oldRoomSelectionSnapshot = $this->bookingService->getOrCreateBaselineSnapshot($request, $bookingCreateDto);
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto); $summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
@@ -52,9 +52,13 @@ class CreateStep1Controller extends AbstractController
if ($this->bookingService->hasRoomSelectionChanged($oldRoomSelectionSnapshot, $bookingCreateDto)) { if ($this->bookingService->hasRoomSelectionChanged($oldRoomSelectionSnapshot, $bookingCreateDto)) {
$this->bookingService->resetParticipantAssignments($bookingCreateDto); $this->bookingService->resetParticipantAssignments($bookingCreateDto);
} }
$bookingCreateDto->currentStep = 2; $bookingCreateDto->currentStep = 2;
$this->bookingService->saveBookingCreateDto($request, $bookingCreateDto); $this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
// Clear baseline snapshot when moving to step 2
$this->bookingService->clearBaselineSnapshot($request);
return $this->redirectToRoute('app_booking_create_step_2'); return $this->redirectToRoute('app_booking_create_step_2');
} }
+9 -7
View File
@@ -5,7 +5,8 @@ namespace App\Form;
use App\BusProNet\Form\CountryType; use App\BusProNet\Form\CountryType;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
use App\Form\Model\ParticipantDto; use App\Form\Model\ParticipantDto;
use App\Form\Service\ParticipantFieldOptionsProvider; use App\Form\Service\Contract\FieldOptionsProviderInterface;
use App\Form\Service\CreateFieldStateProvider;
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;
@@ -20,7 +21,8 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class BookingCreateParticipantType extends AbstractType class BookingCreateParticipantType extends AbstractType
{ {
public function __construct( public function __construct(
private readonly ParticipantFieldOptionsProvider $fieldOptionsProvider, private readonly FieldOptionsProviderInterface $fieldOptionsProvider,
private readonly CreateFieldStateProvider $fieldStateProvider,
) { ) {
} }
@@ -85,7 +87,7 @@ class BookingCreateParticipantType extends AbstractType
} }
// Get the booking DTO from the root form // Get the booking DTO from the root form
$bookingDto = $this->fieldOptionsProvider->getBookingDtoFromForm($form); $bookingDto = $this->fieldStateProvider->getBookingDtoFromForm($form);
if (null === $bookingDto) { if (null === $bookingDto) {
return; return;
@@ -108,7 +110,7 @@ class BookingCreateParticipantType extends AbstractType
} }
// Get the booking DTO from the root form // Get the booking DTO from the root form
$bookingDto = $this->fieldOptionsProvider->getBookingDtoFromForm($form); $bookingDto = $this->fieldStateProvider->getBookingDtoFromForm($form);
if (null === $bookingDto) { if (null === $bookingDto) {
return; return;
@@ -138,7 +140,7 @@ class BookingCreateParticipantType extends AbstractType
private function applyFieldStates(FormInterface $form, BookingDtoInterface $bookingDto, 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($bookingDto, $participantIndex, $formData); $allFieldStates = $this->fieldStateProvider->getAllFieldStates($bookingDto, $participantIndex, $formData);
foreach ($allFieldStates as $fieldName => $fieldState) { foreach ($allFieldStates as $fieldName => $fieldState) {
if ($form->has($fieldName)) { if ($form->has($fieldName)) {
@@ -169,8 +171,8 @@ class BookingCreateParticipantType extends AbstractType
$fieldOptions = $this->fieldOptionsProvider->getFieldOptions($fieldName, $bookingDto, $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->fieldStateProvider->hasStateConditions($fieldName)) {
$fieldState = $this->fieldOptionsProvider->getFieldState($fieldName, $bookingDto, $participantIndex); $fieldState = $this->fieldStateProvider->getFieldState($fieldName, $bookingDto, $participantIndex);
$fieldOptions = $this->mergeFieldState($fieldOptions, $fieldState); $fieldOptions = $this->mergeFieldState($fieldOptions, $fieldState);
} }
+1 -1
View File
@@ -3,7 +3,7 @@
namespace App\Form; namespace App\Form;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingCreateDto;
use App\Form\ParticipantFieldHandler\ParticipantFieldHandlerRegistry; use App\Form\Service\ParticipantFieldHandlerRegistry;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
+1 -1
View File
@@ -5,7 +5,7 @@ namespace App\Form;
use App\BusProNet\Constants; use App\BusProNet\Constants;
use App\BusProNet\Model\Service; use App\BusProNet\Model\Service;
use App\Form\Model\BookingEditDto; use App\Form\Model\BookingEditDto;
use App\Form\ParticipantFieldHandler\ParticipantFieldHandlerRegistry; use App\Form\Service\ParticipantFieldHandlerRegistry;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
+3 -5
View File
@@ -23,12 +23,10 @@ class RoomSelectType extends AbstractType
$form->add('quantity', ChoiceType::class, [ $form->add('quantity', ChoiceType::class, [
'label' => 'Anzahl '.$data->roomLabel, 'label' => 'Anzahl '.$data->roomLabel,
'required' => false, 'required' => true,
'placeholder' => '-', 'choices' => ['-' => 0] + array_combine(range(1, $data->maxQuantity), range(1, $data->maxQuantity)),
'choices' => array_combine(range(1, $data->maxQuantity), range(1, $data->maxQuantity)),
]); ]);
}) });
;
} }
public function configureOptions(OptionsResolver $resolver): void public function configureOptions(OptionsResolver $resolver): void
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace App\Form\Service\Abstract;
use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Contract\FieldOptionsProviderInterface;
/**
* Abstract base class for field options providers.
*
* This class contains common field options generation logic shared between
* different field options provider implementations. It provides the core
* functionality for managing field option providers and generating dynamic
* field configurations.
*/
abstract class AbstractFieldOptionsProvider implements FieldOptionsProviderInterface
{
/** @var array<string, callable> Field option providers indexed by field name */
protected array $fieldOptionProviders = [];
public function __construct()
{
$this->registerFieldOptionProviders();
}
/**
* Retrieves form field options for a specified dynamic field.
*
* This method looks up the appropriate option provider for the field
* and executes it with the current booking and participant context to
* generate dynamic field options.
*
* The returned array contains Symfony form field options that will be
* used when building the form field. These options are merged with any
* static options defined in the form type.
*
* @param string $fieldName The name of the field to configure
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
* @param int $participantIndex The index of the participant being configured
*
* @return array<string, mixed> Symfony form field options, or empty array if field not supported
*/
public function getFieldOptions(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex): array
{
// Check if we have a provider for this field
if (!isset($this->fieldOptionProviders[$fieldName])) {
return [];
}
// Execute the provider with current context to generate dynamic options
return $this->fieldOptionProviders[$fieldName]($bookingDto, $participantIndex);
}
/**
* Checks whether a field has option provider support.
*
* This method allows form builders to determine if a field can be
* dynamically configured by this service. It's useful for deciding
* whether to use static field options or dynamic configuration.
*
* @param string $fieldName The name of the field to check
*
* @return bool True if the field has registered option providers, false otherwise
*/
public function hasFieldOptions(string $fieldName): bool
{
return isset($this->fieldOptionProviders[$fieldName]);
}
/**
* Registers field option providers during service initialization.
*
* This method must be implemented by concrete classes to define their
* specific field option providers. Each provider is a callable that
* receives the booking DTO and participant index and returns appropriate
* Symfony form field options.
*
* Example implementation:
*
* protected function registerFieldOptionProviders(): void
* {
* $this->fieldOptionProviders['fieldName'] = fn($bookingDto, $participantIndex) => [
* 'label' => 'Field Label',
* 'choices' => $this->generateChoicesFor($bookingDto, $participantIndex),
* ];
* }
*/
abstract protected function registerFieldOptionProviders(): void;
}
@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace App\Form\Service\Abstract;
use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Contract\FieldConditionInterface;
use App\Form\Service\Contract\FieldStateProviderInterface;
use App\Form\Service\Trait\FormTraversalTrait;
/**
* Abstract base class for field state providers.
*
* This class contains common field state evaluation logic shared between
* different field state provider implementations. It provides the core
* functionality for evaluating field conditions and applying state modifications.
*/
abstract class AbstractFieldStateProvider implements FieldStateProviderInterface
{
use FormTraversalTrait;
/** @var array<string, array<string, FieldConditionInterface>> Field state conditions indexed by field name and state type */
protected array $fieldStateConditions = [];
public function __construct()
{
$this->registerFieldStateConditions();
}
/**
* Calculates the dynamic state for a specified field.
*
* Evaluates all configured conditions for a field and returns the appropriate
* state modifications. State conditions are organized by state type (readonly,
* disabled, etc.) and evaluated independently.
*
* @param string $fieldName The name of the field to evaluate
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation
*
* @return array<string, mixed> Symfony form field options for state modifications
*/
public function getFieldState(string $fieldName, 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;
}
/**
* Checks whether a field has state conditions configured.
*
* @param string $fieldName The name of the field to check
*
* @return bool True if the field has state conditions configured
*/
public function hasStateConditions(string $fieldName): bool
{
return isset($this->fieldStateConditions[$fieldName]) && !empty($this->fieldStateConditions[$fieldName]);
}
/**
* Returns field names that trigger state re-evaluation for a given field.
*
* @param string $fieldName The name of the field to get dependencies for
*
* @return string[] Array of field names that affect the specified field's state
*/
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);
}
/**
* Calculates field states for all configured fields at once.
*
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation
*
* @return array<string, array<string, mixed>> Field states indexed by field name
*/
public function getAllFieldStates(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;
}
/**
* Registers field state conditions during service initialization.
*
* This method must be implemented by concrete classes to define their
* specific field state conditions.
*/
abstract protected function registerFieldStateConditions(): void;
}
@@ -2,9 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler; namespace App\Form\Service\Abstract;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Contract\ParticipantFieldHandlerInterface;
/** /**
* Abstract base class providing common functionality for participant field handlers. * Abstract base class providing common functionality for participant field handlers.
@@ -2,9 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition; namespace App\Form\Service\Condition;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Contract\FieldConditionInterface;
/** /**
* Condition that evaluates participant age against specified range criteria. * Condition that evaluates participant age against specified range criteria.
@@ -2,9 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition; namespace App\Form\Service\Condition;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Contract\FieldConditionInterface;
/** /**
* Condition that checks if the participant is the applicant. * Condition that checks if the participant is the applicant.
@@ -2,9 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition; namespace App\Form\Service\Condition;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Contract\FieldConditionInterface;
/** /**
* Composite condition that combines multiple conditions with logical operators. * Composite condition that combines multiple conditions with logical operators.
@@ -2,9 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition; namespace App\Form\Service\Condition;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Contract\FieldConditionInterface;
/** /**
* Condition that evaluates field states based on other field values. * Condition that evaluates field states based on other field values.
@@ -2,9 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition; namespace App\Form\Service\Condition;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Contract\FieldConditionInterface;
/** /**
* Condition that checks if a participant's personal data is mutable in the edit flow. * Condition that checks if a participant's personal data is mutable in the edit flow.
@@ -2,9 +2,8 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler\Condition; namespace App\Form\Service\Contract;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
/** /**
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Form\Service\Contract;
use App\Form\Model\BookingDtoInterface;
/**
* Interface for providing dynamic field options based on context.
*
* Field option providers generate context-aware Symfony form field options
* for dynamic fields that require their configuration to be calculated based
* on the current booking state, participant data, and business logic.
*
* Key Responsibilities:
* - Generate dynamic field options based on booking context
* - Support context-aware field configurations
* - Enable extensible field option generation
* - Provide lazy evaluation of field options
*
* Field options typically include:
* - 'label': The field label text
* - 'placeholder': Placeholder text for input fields
* - 'choices': Available choices for choice fields
* - 'choice_loader': Dynamic choice loader for complex choices
* - 'disabled': Whether the field should be disabled
* - 'required': Whether the field is required
* - 'attr': HTML attributes for the field
*/
interface FieldOptionsProviderInterface
{
/**
* Retrieves form field options for a specified dynamic field.
*
* This method looks up the appropriate option provider for the field
* and executes it with the current booking and participant context to
* generate dynamic field options.
*
* The returned array contains Symfony form field options that will be
* used when building the form field. These options are merged with any
* static options defined in the form type.
*
* @param string $fieldName The name of the field to configure
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
* @param int $participantIndex The index of the participant being configured
*
* @return array<string, mixed> Symfony form field options, or empty array if field not supported
*/
public function getFieldOptions(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex): array;
/**
* Checks whether a field has option provider support.
*
* This method allows form builders to determine if a field can be
* dynamically configured by this service. It's useful for deciding
* whether to use static field options or dynamic configuration.
*
* @param string $fieldName The name of the field to check
*
* @return bool True if the field has registered option providers, false otherwise
*/
public function hasFieldOptions(string $fieldName): bool;
}
@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Form\Service; namespace App\Form\Service\Contract;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler; namespace App\Form\Service\Contract;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Form\Service;
use App\Form\Service\Abstract\AbstractFieldStateProvider;
/**
* Field state provider for the booking create workflow.
*
* This service calculates dynamic field states (readonly, disabled, etc.)
* for participant fields in the create flow. It extends the common field
* state functionality provided by AbstractFieldStateProvider.
*
* Currently, no specific field state conditions are implemented for the
* create workflow, but the infrastructure is ready for future additions.
*/
class CreateFieldStateProvider extends AbstractFieldStateProvider
{
/**
* Registers field state conditions for the create workflow.
*
* This method defines the conditional logic for field states in the
* booking creation process. Each field can have multiple state conditions
* (readonly, disabled, hidden, required) that are evaluated independently.
*
* Adding new field state conditions:
* To add conditional state logic for a field, register conditions here:
*
* $this->fieldStateConditions['fieldName'] = [
* 'readonly' => new SomeCondition(),
* 'disabled' => CompositeCondition::and(
* new AgeRangeCondition(null, 17),
* FieldValueCondition::equals('someField', 'someValue')
* ),
* ];
*
* Condition Types:
* - 'readonly': Field is visible but not editable
* - 'disabled': Field interaction is disabled
* - 'required': Field becomes mandatory
* - 'hidden': Field is not displayed
*/
protected function registerFieldStateConditions(): void
{
// Example field state conditions would be registered here
// For demonstration purposes, here are some example patterns:
// Example 1: Make assignedRoomId readonly for participants under 18
// $this->fieldStateConditions['assignedRoomId'] = [
// 'readonly' => new AgeRangeCondition(null, 17),
// ];
// Example 2: Disable service selection if no room is assigned
// $this->fieldStateConditions['serviceSelection'] = [
// 'disabled' => FieldValueCondition::isEmpty('assignedRoomId'),
// ];
// Example 3: Complex condition with multiple criteria
// $this->fieldStateConditions['advancedOptions'] = [
// 'hidden' => CompositeCondition::or(
// new AgeRangeCondition(null, 15),
// FieldValueCondition::equals('userType', 'basic')
// ),
// ];
}
}
+11 -90
View File
@@ -4,11 +4,10 @@ declare(strict_types=1);
namespace App\Form\Service; namespace App\Form\Service;
use App\Form\Model\BookingDtoInterface; use App\Form\Service\Condition\ApplicantCondition;
use App\Form\ParticipantFieldHandler\Condition\ApplicantCondition; use App\Form\Service\Condition\CompositeCondition;
use App\Form\ParticipantFieldHandler\Condition\CompositeCondition; use App\Form\Service\Condition\MutabilityCondition;
use App\Form\ParticipantFieldHandler\Condition\FieldConditionInterface; use App\Form\Service\Abstract\AbstractFieldStateProvider;
use App\Form\ParticipantFieldHandler\Condition\MutabilityCondition;
/** /**
* Field state provider for the booking edit workflow. * Field state provider for the booking edit workflow.
@@ -16,27 +15,19 @@ use App\Form\ParticipantFieldHandler\Condition\MutabilityCondition;
* This service calculates dynamic field states (readonly, disabled, etc.) * This service calculates dynamic field states (readonly, disabled, etc.)
* for participant fields in the edit flow, using edit-specific conditions. * for participant fields in the edit flow, using edit-specific conditions.
* *
* It is designed to be extensible and composable, allowing reuse of * It extends the common field state functionality provided by
* existing condition classes and easy registration of new logic. * AbstractFieldStateProvider and adds edit-specific field state logic.
*/ */
class EditFieldStateProvider implements FieldStateProviderInterface class EditFieldStateProvider extends AbstractFieldStateProvider
{ {
use FormTraversalTrait;
/** @var array<string, array<string, FieldConditionInterface>> */
private array $fieldStateConditions = [];
public function __construct()
{
$this->registerFieldStateConditions();
}
/** /**
* Registers field state conditions for the edit workflow. * Registers field state conditions for the edit workflow.
* *
* Add or modify conditions as needed for your domain. * This method defines the conditional logic for field states in the
* booking edit process. It makes personal data fields readonly when
* the participant is the applicant or when the field is not mutable.
*/ */
private function registerFieldStateConditions(): void protected function registerFieldStateConditions(): void
{ {
// Make all personal data fields readonly if not mutable OR if applicant // Make all personal data fields readonly if not mutable OR if applicant
$personalDataFields = [ $personalDataFields = [
@@ -56,75 +47,5 @@ class EditFieldStateProvider implements FieldStateProviderInterface
), ),
]; ];
} }
// 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;
} }
} }
@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Form\Service; namespace App\Form\Service\Factory;
use App\Form\ChoiceLoader\ParticipantRoomChoiceLoader; use App\Form\ChoiceLoader\ParticipantRoomChoiceLoader;
use App\Form\Model\ParticipantDto; use App\Form\Model\ParticipantDto;
@@ -2,9 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler; namespace App\Form\Service;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
/** /**
* Handles processing of the assignedRoomId field for booking participants. * Handles processing of the assignedRoomId field for booking participants.
@@ -2,9 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Form\ParticipantFieldHandler; namespace App\Form\Service;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Contract\ParticipantFieldHandlerInterface;
/** /**
* Registry for managing and executing participant field handlers in dependency order. * Registry for managing and executing participant field handlers in dependency order.
@@ -6,98 +6,42 @@ namespace App\Form\Service;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
use App\Form\ParticipantFieldHandler\Condition\FieldConditionInterface; use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory;
/** /**
* Provides dynamic field options and state for participant form fields. * Provides dynamic field options for participant form fields.
* *
* This service acts as both a field option provider and field state provider * This service generates context-aware Symfony form field options for
* for the participant form system. It generates context-aware Symfony form * dynamic fields in the participant form system. It handles fields that
* field options and calculates dynamic field states based on conditional logic. * require options to be calculated based on the current booking context,
* participant data, and business logic.
* *
* Key Responsibilities: * Key Responsibilities:
* - Manages field option providers for dynamic fields * - Manages field option providers for dynamic fields
* - Calculates field states based on conditional logic
* - Provides context-aware field configurations * - Provides context-aware field configurations
* - Handles interdependent field relationships * - Handles interdependent field relationships
* - Supports extensible field option and state generation * - Supports extensible field option generation
* *
* The provider uses a callable pattern for field options and a condition-based * The provider uses a callable pattern for field options, enabling
* system for field states, enabling complex conditional field behavior. * lazy evaluation and complex conditional field behavior.
*/ */
class ParticipantFieldOptionsProvider implements FieldStateProviderInterface class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
{ {
use FormTraversalTrait;
/** @var array<string, callable> Field option providers indexed by field name */
private array $fieldOptionProviders = [];
/** @var array<string, array<string, FieldConditionInterface>> Field state conditions indexed by field name and state type */
private array $fieldStateConditions = [];
/** /**
* Initializes the provider with required dependencies. * Initializes the provider with required dependencies.
* *
* The provider automatically registers all field option providers and * The provider automatically registers all field option providers during
* field state conditions during construction to ensure they're available * construction to ensure they're available for form building. This approach
* for form building. This approach keeps all field configuration logic * keeps all field configuration logic centralized and makes it easy to add
* centralized and makes it easy to add new dynamic fields. * new dynamic fields.
* *
* @param ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory Factory for creating room choice loaders * @param ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory Factory for creating room choice loaders
*/ */
public function __construct( public function __construct(
private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory, private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory,
) { ) {
$this->registerFieldOptionProviders(); parent::__construct();
$this->registerFieldStateConditions();
}
/**
* Retrieves form field options for a specified dynamic field.
*
* This is the main entry point for getting field configurations. It looks up
* the appropriate option provider for the field and executes it with the
* current booking and participant context to generate dynamic field options.
*
* The returned array contains Symfony form field options such as:
* - 'label' - The field label
* - 'placeholder' - Placeholder text
* - 'choices' - Available choices for choice fields
* - 'choice_loader' - Dynamic choice loader for complex choices
* - 'disabled' - Whether the field should be disabled
* - 'required' - Whether the field is required
*
* @param string $fieldName The name of the field to configure
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
* @param int $participantIndex The index of the participant being configured
*
* @return array<string, mixed> Symfony form field options, or empty array if field not supported
*/
public function getFieldOptions(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex): array
{
// Check if we have a provider for this field
if (!isset($this->fieldOptionProviders[$fieldName])) {
return [];
}
// Execute the provider with current context to generate dynamic options
return $this->fieldOptionProviders[$fieldName]($bookingDto, $participantIndex);
}
/**
* Checks whether a field has option provider support.
*
* This method allows form builders to determine if a field can be
* dynamically configured by this service. It's useful for deciding
* whether to use static field options or dynamic configuration.
*
* @param string $fieldName The name of the field to check
*
* @return bool True if the field has registered option providers, false otherwise
*/
public function hasFieldOptions(string $fieldName): bool
{
return isset($this->fieldOptionProviders[$fieldName]);
} }
/** /**
@@ -121,7 +65,7 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
* - Easy to test individual field logic * - Easy to test individual field logic
* - Supports complex interdependencies * - Supports complex interdependencies
*/ */
private function registerFieldOptionProviders(): void protected function registerFieldOptionProviders(): void
{ {
// Room assignment field provider (only available for create workflow) // Room assignment field provider (only available for create workflow)
$this->fieldOptionProviders['assignedRoomId'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ $this->fieldOptionProviders['assignedRoomId'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
@@ -151,158 +95,4 @@ class ParticipantFieldOptionsProvider implements FieldStateProviderInterface
// ], // ],
// ]; // ];
} }
/**
* Calculates the dynamic state for a specified field.
*
* Evaluates all configured conditions for a field and returns the appropriate
* state modifications. State conditions are organized by state type (readonly,
* disabled, etc.) and evaluated independently.
*
* @param string $fieldName The name of the field to evaluate
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation
*
* @return array<string, mixed> Symfony form field options for state modifications
*/
public function getFieldState(string $fieldName, 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;
}
/**
* Checks whether a field has state conditions configured.
*
* @param string $fieldName The name of the field to check
*
* @return bool True if the field has state conditions configured
*/
public function hasStateConditions(string $fieldName): bool
{
return isset($this->fieldStateConditions[$fieldName]) && !empty($this->fieldStateConditions[$fieldName]);
}
/**
* Returns field names that trigger state re-evaluation for a given field.
*
* @param string $fieldName The name of the field to get dependencies for
*
* @return string[] Array of field names that affect the specified field's state
*/
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);
}
/**
* Calculates field states for all configured fields at once.
*
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
* @param int $participantIndex The index of the participant being evaluated
* @param array<string, mixed> $formData Current form data for condition evaluation
*
* @return array<string, array<string, mixed>> Field states indexed by field name
*/
public function getAllFieldStates(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;
}
/**
* Registers field state conditions during service initialization.
*
* This method defines the conditional logic for field states. Each field
* can have multiple state conditions (readonly, disabled, hidden, required)
* that are evaluated independently.
*
* Adding new field state conditions:
* To add conditional state logic for a field, register conditions here:
*
* $this->fieldStateConditions['fieldName'] = [
* 'readonly' => new SomeCondition(),
* 'disabled' => CompositeCondition::and(
* new AgeRangeCondition(null, 17),
* FieldValueCondition::equals('someField', 'someValue')
* ),
* ];
*
* Condition Types:
* - 'readonly': Field is visible but not editable
* - 'disabled': Field interaction is disabled
* - 'required': Field becomes mandatory
* - 'hidden': Field is not displayed
*/
private function registerFieldStateConditions(): void
{
// Example field state conditions would be registered here
// For demonstration purposes, here are some example patterns:
// Example 1: Make assignedRoomId readonly for participants under 18
// $this->fieldStateConditions['assignedRoomId'] = [
// 'readonly' => new AgeRangeCondition(null, 17),
// ];
// Example 2: Disable service selection if no room is assigned
// $this->fieldStateConditions['serviceSelection'] = [
// 'disabled' => FieldValueCondition::isEmpty('assignedRoomId'),
// ];
// Example 3: Complex condition with multiple criteria
// $this->fieldStateConditions['advancedOptions'] = [
// 'hidden' => CompositeCondition::or(
// new AgeRangeCondition(null, 15),
// FieldValueCondition::equals('userType', 'basic')
// ),
// ];
}
} }
@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Form\Service; namespace App\Form\Service\Trait;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
+45 -7
View File
@@ -11,14 +11,51 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class BookingService class BookingService
{ {
public const BOOKING_CREATE_KEY = 'booking_create';
public const BOOKING_CREATE_BASELINE_KEY = 'booking_create_baseline_snapshot';
public function __construct( public function __construct(
private readonly TravelDataService $travelDataService, private readonly TravelDataService $travelDataService,
) {} ) {
}
/**
* Gets or creates the baseline room selection snapshot for change detection.
*
* The baseline snapshot captures the initial room selection state when step 1
* is first loaded, before any HTMX modifications. This ensures accurate change
* detection for room assignment resets.
*/
public function getOrCreateBaselineSnapshot(Request $request, BookingCreateDto $bookingCreateDto): array
{
$baselineKey = self::BOOKING_CREATE_BASELINE_KEY;
if (!$request->getSession()->has($baselineKey) || $request->query->has('reset_baseline')) {
$baseline = $this->createRoomSelectionSnapshot($bookingCreateDto);
$request->getSession()->set($baselineKey, $baseline);
return $baseline;
}
return $request->getSession()->get($baselineKey);
}
/**
* Clears the baseline snapshot from the session.
*
* Should be called when moving to the next step or when the baseline
* needs to be refreshed.
*/
public function clearBaselineSnapshot(Request $request): void
{
$request->getSession()->remove('booking_create_baseline_snapshot');
}
public function getOrCreateBookingCreateDto(Request $request): BookingCreateDto public function getOrCreateBookingCreateDto(Request $request): BookingCreateDto
{ {
$bookingCreateKey = self::BOOKING_CREATE_KEY;
$bookingUuid = $request->query->get('uid'); $bookingUuid = $request->query->get('uid');
$bookingCreateDto = $request->getSession()->get('booking_create'); $bookingCreateDto = $request->getSession()->get($bookingCreateKey);
// No UID parameter - return existing DTO from session if available // No UID parameter - return existing DTO from session if available
if (null === $bookingUuid && null !== $bookingCreateDto) { if (null === $bookingUuid && null !== $bookingCreateDto) {
@@ -41,7 +78,7 @@ class BookingService
$availableRooms = $travelData->getAvailableRooms(); $availableRooms = $travelData->getAvailableRooms();
$roomSelections = array_map( $roomSelections = array_map(
fn(Room $room) => $this->createRoomSelection($room, $roomsIdsAndQuantities), fn (Room $room) => $this->createRoomSelection($room, $roomsIdsAndQuantities),
$availableRooms $availableRooms
); );
@@ -55,7 +92,7 @@ class BookingService
public function saveBookingCreateDto(Request $request, BookingCreateDto $bookingCreateDto): void public function saveBookingCreateDto(Request $request, BookingCreateDto $bookingCreateDto): void
{ {
$request->getSession()->set('booking_create', $bookingCreateDto); $request->getSession()->set(self::BOOKING_CREATE_KEY, $bookingCreateDto);
} }
private function createRoomSelection(Room $room, array $roomsIdsAndQuantities): RoomSelectionDto private function createRoomSelection(Room $room, array $roomsIdsAndQuantities): RoomSelectionDto
@@ -188,8 +225,8 @@ class BookingService
*/ */
public function shouldResetAssignments(BookingCreateDto $oldDto, BookingCreateDto $newDto): bool public function shouldResetAssignments(BookingCreateDto $oldDto, BookingCreateDto $newDto): bool
{ {
$old = array_map(fn($roomSelectionDto) => [$roomSelectionDto->roomId, $roomSelectionDto->quantity], $oldDto->roomSelections); $old = array_map(fn ($roomSelectionDto) => [$roomSelectionDto->roomId, $roomSelectionDto->quantity], $oldDto->roomSelections);
$new = array_map(fn($roomSelectionDto) => [$roomSelectionDto->roomId, $roomSelectionDto->quantity], $newDto->roomSelections); $new = array_map(fn ($roomSelectionDto) => [$roomSelectionDto->roomId, $roomSelectionDto->quantity], $newDto->roomSelections);
return $old !== $new; return $old !== $new;
} }
@@ -212,7 +249,7 @@ class BookingService
public function createRoomSelectionSnapshot(BookingCreateDto $dto): array public function createRoomSelectionSnapshot(BookingCreateDto $dto): array
{ {
return array_map( return array_map(
fn($roomSelection) => [$roomSelection->roomId, $roomSelection->quantity], fn ($roomSelection) => [(int) $roomSelection->roomId, (int) $roomSelection->quantity],
$dto->roomSelections $dto->roomSelections
); );
} }
@@ -223,6 +260,7 @@ class BookingService
public function hasRoomSelectionChanged(array $oldSnapshot, BookingCreateDto $newDto): bool public function hasRoomSelectionChanged(array $oldSnapshot, BookingCreateDto $newDto): bool
{ {
$newSnapshot = $this->createRoomSelectionSnapshot($newDto); $newSnapshot = $this->createRoomSelectionSnapshot($newDto);
return $oldSnapshot !== $newSnapshot; return $oldSnapshot !== $newSnapshot;
} }
} }
+2 -2
View File
@@ -9,7 +9,7 @@
<li> <li>
{{ roomSelection.quantity }} x {{ roomSelection.roomLabel }} {{ roomSelection.quantity }} x {{ roomSelection.roomLabel }}
{% if assignmentCounts is defined and assignmentCounts[roomSelection.roomId] is defined %} {% if assignmentCounts is defined and assignmentCounts[roomSelection.roomId] is defined %}
<span class="block text-sm">{{ assignmentCounts[roomSelection.roomId] }}/{{ roomSelection.quantity }} belegt</span> <span class="block text-sm">{{ assignmentCounts[roomSelection.roomId] }} belegt</span>
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
@@ -22,7 +22,7 @@
<li> <li>
{{ roomSelection.quantity }} x {{ roomSelection.roomLabel }} {{ roomSelection.quantity }} x {{ roomSelection.roomLabel }}
{% if assignmentCounts is defined and assignmentCounts[roomSelection.roomId] is defined %} {% if assignmentCounts is defined and assignmentCounts[roomSelection.roomId] is defined %}
<span class="block text-sm">{{ assignmentCounts[roomSelection.roomId] }}/{{ roomSelection.quantity }} belegt</span> <span class="block text-sm">{{ assignmentCounts[roomSelection.roomId] }} belegt</span>
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
+1 -1
View File
@@ -13,7 +13,7 @@
<div id="participants-form" class="space-y-8 pb-8"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}> <div id="participants-form" class="space-y-8 pb-8"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}>
{% for participant in form.participants %} {% for participant in form.participants %}
{% set participantDataValid = participant.vars.valid %} {% set participantDataValid = participant.vars.valid %}
<div class="{{ html_classes('border rounded px-4 py-2', { 'border-gray-300': participantDataValid, 'border-red-700': not participantDataValid }) }}" {{ stimulus_controller('toggle', {'open': participant.vars.valid == false}, { 'closed': 'hidden' }) }}> <div class="{{ html_classes('border rounded px-4 py-2', { 'border-gray-300': participantDataValid, 'border-red-700': not participantDataValid }) }}" {{ stimulus_controller('toggle', {'storageKey': 'participant_' ~ loop.index0, 'open': participant.vars.valid == false}, { 'closed': 'hidden' }) }}>
<fieldset> <fieldset>
<legend class="w-full flex items-center justify-between"> <legend class="w-full flex items-center justify-between">
<span class="font-bold text-xl">Teilnehmer:in {{ loop.index }}</span> <span class="font-bold text-xl">Teilnehmer:in {{ loop.index }}</span>
@@ -212,10 +212,7 @@ class BookingDataProcessorTest extends TestCase
private function createCompleteFormData(): BookingEditDto private function createCompleteFormData(): BookingEditDto
{ {
$formData = new BookingEditDto(); $formData = new BookingEditDto($this->createMockBooking(), $this->createMockTravel());
$formData->booking = $this->createMockBooking();
$formData->travel = $this->createMockTravel();
$formData->participants = [ $formData->participants = [
$this->createMockParticipantDto(0, 'F'), $this->createMockParticipantDto(0, 'F'),
$this->createMockParticipantDto(1, 'F'), $this->createMockParticipantDto(1, 'F'),
@@ -226,10 +223,7 @@ class BookingDataProcessorTest extends TestCase
private function createFormDataWithCanceledParticipant(): BookingEditDto private function createFormDataWithCanceledParticipant(): BookingEditDto
{ {
$formData = new BookingEditDto(); $formData = new BookingEditDto($this->createMockBooking(), $this->createMockTravel());
$formData->booking = $this->createMockBooking();
$formData->travel = $this->createMockTravel();
$formData->participants = [ $formData->participants = [
$this->createMockParticipantDto(0, 'F'), $this->createMockParticipantDto(0, 'F'),
$this->createMockParticipantDto(1, 'S'), // Canceled $this->createMockParticipantDto(1, 'S'), // Canceled