wip: age based filtering of options
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
/**
|
||||
* Represents the result of parsing age constraint data from XML.
|
||||
*
|
||||
* This DTO holds the parsed age constraint information including both absolute age
|
||||
* constraints and birth year constraints, along with metadata for extensibility.
|
||||
*/
|
||||
class AgeConstraintResult
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $type,
|
||||
public readonly ?int $ageFrom = null,
|
||||
public readonly ?int $ageTo = null,
|
||||
public readonly ?int $birthYearFrom = null,
|
||||
public readonly ?int $birthYearTo = null,
|
||||
public readonly array $metadata = [],
|
||||
public readonly ?string $rawData = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function hasAgeConstraints(): bool
|
||||
{
|
||||
return null !== $this->ageFrom || null !== $this->ageTo;
|
||||
}
|
||||
|
||||
public function hasBirthYearConstraints(): bool
|
||||
{
|
||||
return null !== $this->birthYearFrom || null !== $this->birthYearTo;
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return false === $this->hasAgeConstraints() && false === $this->hasBirthYearConstraints();
|
||||
}
|
||||
}
|
||||
@@ -72,4 +72,25 @@ class Service
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $direction = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?int $ageFrom = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?int $ageTo = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?int $birthYearFrom = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?int $birthYearTo = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $ageConstraintType = null;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?array $ageConstraintMetadata = null;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $rawAgeConstraintData = null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Model\AgeConstraintResult;
|
||||
|
||||
/**
|
||||
* Interface for parsing age constraint data from XML.
|
||||
*
|
||||
* This interface defines the contract for parsers that can handle different
|
||||
* types of age constraint formats (birth year, grade level, etc.).
|
||||
* Implementations should be able to determine if they can parse a given
|
||||
* constraint string and return structured constraint information.
|
||||
*/
|
||||
interface AgeConstraintParserInterface
|
||||
{
|
||||
/**
|
||||
* Determines if this parser can handle the given constraint data.
|
||||
*
|
||||
* @param string $constraintData The constraint data to evaluate (e.g., 'JG:2007-2009')
|
||||
*
|
||||
* @return bool True if this parser can handle the constraint data
|
||||
*/
|
||||
public function canParse(string $constraintData): bool;
|
||||
|
||||
/**
|
||||
* Parses the constraint data into structured information.
|
||||
*
|
||||
* @param string $constraintData The constraint data to parse
|
||||
*
|
||||
* @return AgeConstraintResult The parsed constraint information
|
||||
*
|
||||
* @throws \InvalidArgumentException If the parser cannot handle the constraint data
|
||||
*/
|
||||
public function parse(string $constraintData): AgeConstraintResult;
|
||||
|
||||
/**
|
||||
* Returns the constraint type handled by this parser.
|
||||
*
|
||||
* @return string The constraint type identifier (e.g., 'birth_year', 'grade_level')
|
||||
*/
|
||||
public function getConstraintType(): string;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Model\AgeConstraintResult;
|
||||
|
||||
/**
|
||||
* Registry for managing and executing age constraint parsers.
|
||||
*
|
||||
* This registry coordinates multiple age constraint parsers and can handle
|
||||
* complex constraint data with multiple types separated by semicolons
|
||||
* (e.g., 'JG:2007-2009;GL:5-8').
|
||||
*/
|
||||
class AgeConstraintParserRegistry
|
||||
{
|
||||
/** @var AgeConstraintParserInterface[] */
|
||||
private array $parsers = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Register built-in parsers
|
||||
$this->addParser(new BirthYearConstraintParser());
|
||||
}
|
||||
|
||||
public function addParser(AgeConstraintParserInterface $parser): void
|
||||
{
|
||||
$this->parsers[] = $parser;
|
||||
}
|
||||
|
||||
public function parseConstraints(string $constraintData): AgeConstraintResult
|
||||
{
|
||||
// Handle multiple constraint types (semicolon-separated, e.g., 'JG:2007-2009;GL:5-8')
|
||||
$constraints = array_map('trim', explode(';', $constraintData));
|
||||
$results = [];
|
||||
|
||||
foreach ($constraints as $constraint) {
|
||||
if (empty($constraint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->parsers as $parser) {
|
||||
if (true === $parser->canParse($constraint)) {
|
||||
$results[] = $parser->parse($constraint);
|
||||
break; // First matching parser wins
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge results if multiple constraints found
|
||||
return $this->mergeConstraintResults($results, $constraintData);
|
||||
}
|
||||
|
||||
private function mergeConstraintResults(array $results, string $rawData): AgeConstraintResult
|
||||
{
|
||||
if (empty($results)) {
|
||||
return new AgeConstraintResult(type: 'unknown', rawData: $rawData);
|
||||
}
|
||||
|
||||
if (1 === count($results)) {
|
||||
return $results[0];
|
||||
}
|
||||
|
||||
// Merge multiple constraint results
|
||||
$type = 'mixed';
|
||||
$ageFrom = null;
|
||||
$ageTo = null;
|
||||
$birthYearFrom = null;
|
||||
$birthYearTo = null;
|
||||
$metadata = ['merged_from' => []];
|
||||
|
||||
foreach ($results as $result) {
|
||||
$ageFrom = $this->mergeMinValue($ageFrom, $result->ageFrom);
|
||||
$ageTo = $this->mergeMaxValue($ageTo, $result->ageTo);
|
||||
$birthYearFrom = $this->mergeMinValue($birthYearFrom, $result->birthYearFrom);
|
||||
$birthYearTo = $this->mergeMaxValue($birthYearTo, $result->birthYearTo);
|
||||
$metadata['merged_from'][] = $result->type;
|
||||
}
|
||||
|
||||
return new AgeConstraintResult(
|
||||
type: $type,
|
||||
ageFrom: $ageFrom,
|
||||
ageTo: $ageTo,
|
||||
birthYearFrom: $birthYearFrom,
|
||||
birthYearTo: $birthYearTo,
|
||||
metadata: $metadata,
|
||||
rawData: $rawData
|
||||
);
|
||||
}
|
||||
|
||||
private function mergeMinValue(?int $current, ?int $new): ?int
|
||||
{
|
||||
if (null === $current) {
|
||||
return $new;
|
||||
}
|
||||
if (null === $new) {
|
||||
return $current;
|
||||
}
|
||||
|
||||
return max($current, $new); // Most restrictive minimum
|
||||
}
|
||||
|
||||
private function mergeMaxValue(?int $current, ?int $new): ?int
|
||||
{
|
||||
if (null === $current) {
|
||||
return $new;
|
||||
}
|
||||
if (null === $new) {
|
||||
return $current;
|
||||
}
|
||||
|
||||
return min($current, $new); // Most restrictive maximum
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Model\AgeConstraintResult;
|
||||
|
||||
/**
|
||||
* Parser for birth year constraint data in JG:YYYY-YYYY format.
|
||||
*
|
||||
* This parser handles constraint data that specifies birth year ranges,
|
||||
* such as 'JG:2007-2009' (Jahrgang 2007 to 2009) or 'JG:2007' (single year).
|
||||
*/
|
||||
class BirthYearConstraintParser implements AgeConstraintParserInterface
|
||||
{
|
||||
private const BIRTH_YEAR_PREFIX = 'JG:';
|
||||
|
||||
public function canParse(string $constraintData): bool
|
||||
{
|
||||
return str_starts_with($constraintData, self::BIRTH_YEAR_PREFIX);
|
||||
}
|
||||
|
||||
public function parse(string $constraintData): AgeConstraintResult
|
||||
{
|
||||
if (false === $this->canParse($constraintData)) {
|
||||
throw new \InvalidArgumentException('Cannot parse constraint data: ' . $constraintData);
|
||||
}
|
||||
|
||||
$yearData = substr($constraintData, strlen(self::BIRTH_YEAR_PREFIX));
|
||||
|
||||
// Parse range format "2007-2009"
|
||||
if (str_contains($yearData, '-')) {
|
||||
[$fromYear, $toYear] = explode('-', $yearData, 2);
|
||||
|
||||
return new AgeConstraintResult(
|
||||
type: 'birth_year',
|
||||
birthYearFrom: (int) trim($fromYear),
|
||||
birthYearTo: (int) trim($toYear),
|
||||
metadata: [
|
||||
'range_type' => 'birth_year_range',
|
||||
'original_format' => $yearData,
|
||||
],
|
||||
rawData: $constraintData
|
||||
);
|
||||
}
|
||||
|
||||
// Parse single year format "2007"
|
||||
$year = (int) trim($yearData);
|
||||
|
||||
return new AgeConstraintResult(
|
||||
type: 'birth_year',
|
||||
birthYearFrom: $year,
|
||||
birthYearTo: $year,
|
||||
metadata: [
|
||||
'range_type' => 'birth_year_single',
|
||||
'original_format' => $yearData,
|
||||
],
|
||||
rawData: $constraintData
|
||||
);
|
||||
}
|
||||
|
||||
public function getConstraintType(): string
|
||||
{
|
||||
return 'birth_year';
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,12 @@ use Symfony\Component\DomCrawler\Crawler;
|
||||
*/
|
||||
class TravelParser extends AbstractParser
|
||||
{
|
||||
private AgeConstraintParserRegistry $ageConstraintRegistry;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ageConstraintRegistry = new AgeConstraintParserRegistry();
|
||||
}
|
||||
/**
|
||||
* Parse XML node into a Travel object.
|
||||
*
|
||||
@@ -129,6 +135,9 @@ class TravelParser extends AbstractParser
|
||||
->stringToFloat($this->getStringOrNullValue($serviceNode->filterXPath('//preis')));
|
||||
$service->status = $this->getStringOrNullValue($serviceNode->filterXPath('//status'));
|
||||
|
||||
// Parse age constraints
|
||||
$this->parseServiceAgeConstraints($serviceNode, $service);
|
||||
|
||||
$additionalServices[$serviceId] = $service;
|
||||
});
|
||||
|
||||
@@ -302,4 +311,59 @@ class TravelParser extends AbstractParser
|
||||
|
||||
return $rooms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse age constraints from service XML node and apply them to the service.
|
||||
*
|
||||
* Handles both absolute age constraints (altervon/alterbis) and extensible
|
||||
* constraint data (hinweis_stamm) that may contain birth year ranges or
|
||||
* future constraint types.
|
||||
*
|
||||
* @param Crawler $serviceNode The XML node containing service data
|
||||
* @param Service $service The service object to populate with constraints
|
||||
*/
|
||||
private function parseServiceAgeConstraints(Crawler $serviceNode, Service $service): void
|
||||
{
|
||||
// Parse absolute age constraints (altervon/alterbis)
|
||||
$ageFrom = $this->getIntOrNullValue($serviceNode->filterXPath('.//altervon'));
|
||||
$ageTo = $this->getIntOrNullValue($serviceNode->filterXPath('.//alterbis'));
|
||||
|
||||
// Parse extensible constraint data (hinweis_stamm)
|
||||
$constraintData = $this->getStringOrNullValue($serviceNode->filterXPath('.//hinweis_stamm'));
|
||||
|
||||
$constraintResult = null;
|
||||
if (null !== $constraintData && '' !== trim($constraintData)) {
|
||||
$constraintResult = $this->ageConstraintRegistry->parseConstraints($constraintData);
|
||||
}
|
||||
|
||||
// Apply absolute age constraints
|
||||
if (null !== $ageFrom || null !== $ageTo) {
|
||||
$service->ageFrom = $ageFrom;
|
||||
$service->ageTo = $ageTo;
|
||||
|
||||
if (null !== $constraintResult && false === $constraintResult->isEmpty()) {
|
||||
// Mixed constraints scenario
|
||||
$service->ageConstraintType = 'mixed';
|
||||
$service->birthYearFrom = $constraintResult->birthYearFrom;
|
||||
$service->birthYearTo = $constraintResult->birthYearTo;
|
||||
$service->ageConstraintMetadata = array_merge(
|
||||
$constraintResult->metadata,
|
||||
['has_absolute_age' => true, 'has_birth_year' => true]
|
||||
);
|
||||
} else {
|
||||
$service->ageConstraintType = 'absolute_age';
|
||||
}
|
||||
} elseif (null !== $constraintResult && false === $constraintResult->isEmpty()) {
|
||||
// Only constraint data (birth year, etc.)
|
||||
$service->ageConstraintType = $constraintResult->type;
|
||||
$service->birthYearFrom = $constraintResult->birthYearFrom;
|
||||
$service->birthYearTo = $constraintResult->birthYearTo;
|
||||
$service->ageConstraintMetadata = $constraintResult->metadata;
|
||||
}
|
||||
|
||||
// Always store raw data for debugging/future parsing
|
||||
if (null !== $constraintData) {
|
||||
$service->rawAgeConstraintData = $constraintData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ class BookingCreateParticipantType extends AbstractType
|
||||
$submittedData = $event->getData();
|
||||
$form = $event->getForm();
|
||||
|
||||
if (!is_array($submittedData)) {
|
||||
if (false === is_array($submittedData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ class BookingCreateParticipantType extends AbstractType
|
||||
|
||||
// Get participant index from form data
|
||||
$participantData = $form->getData();
|
||||
if (null === $participantData || !property_exists($participantData, 'index')) {
|
||||
if (null === $participantData || false === property_exists($participantData, 'index')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ class BookingCreateParticipantType extends AbstractType
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($form->has($fieldName)) {
|
||||
if (true === $form->has($fieldName)) {
|
||||
$field = $form->get($fieldName);
|
||||
$currentOptions = $field->getConfig()->getOptions();
|
||||
|
||||
@@ -168,7 +168,7 @@ class BookingCreateParticipantType extends AbstractType
|
||||
}
|
||||
|
||||
// Apply body dimension field states to the nested bodyDimensions form
|
||||
if (!empty($bodyDimensionStates) && $form->has('bodyDimensions')) {
|
||||
if (false === empty($bodyDimensionStates) && true === $form->has('bodyDimensions')) {
|
||||
$this->applyBodyDimensionStates($form, $bodyDimensionStates);
|
||||
}
|
||||
}
|
||||
@@ -212,17 +212,51 @@ class BookingCreateParticipantType extends AbstractType
|
||||
// Get base field options
|
||||
$fieldOptions = $this->fieldOptionsProvider->getFieldOptions($fieldName, $bookingDto, $participantIndex);
|
||||
|
||||
// Apply dynamic field state if conditions exist
|
||||
if ($this->fieldStateProvider->hasStateConditions($fieldName)) {
|
||||
$fieldState = $this->fieldStateProvider->getFieldState($fieldName, $bookingDto, $participantIndex);
|
||||
$fieldOptions = $this->mergeFieldState($fieldOptions, $fieldState);
|
||||
}
|
||||
// Only add the field if there are actually choices/options available
|
||||
if (true === $this->hasValidFieldOptions($fieldOptions)) {
|
||||
// Apply dynamic field state if conditions exist
|
||||
if (true === $this->fieldStateProvider->hasStateConditions($fieldName)) {
|
||||
$fieldState = $this->fieldStateProvider->getFieldState($fieldName, $bookingDto, $participantIndex);
|
||||
$fieldOptions = $this->mergeFieldState($fieldOptions, $fieldState);
|
||||
}
|
||||
|
||||
$form->add($fieldName, ChoiceType::class, $fieldOptions);
|
||||
$form->add($fieldName, ChoiceType::class, $fieldOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if field options contain valid choices for rendering.
|
||||
*
|
||||
* This method validates that the field options contain either choices array,
|
||||
* choice_loader, or other valid choice sources. Empty choice arrays or
|
||||
* null choice loaders indicate the field should not be rendered.
|
||||
*
|
||||
* @param array<string, mixed> $fieldOptions The field options to validate
|
||||
*
|
||||
* @return bool True if the field has valid options for rendering, false otherwise
|
||||
*/
|
||||
private function hasValidFieldOptions(array $fieldOptions): bool
|
||||
{
|
||||
// Check if choices array exists and is not empty
|
||||
if (false === empty($fieldOptions['choices'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if choice_loader exists and is not null
|
||||
if (isset($fieldOptions['choice_loader'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for other valid choice sources
|
||||
if (isset($fieldOptions['choice_list'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges field state modifications into existing field options.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
* Condition that evaluates whether a participant has provided their date of birth.
|
||||
*
|
||||
* This condition checks if a participant has a valid date of birth set, which is
|
||||
* required for evaluating age-based service constraints and showing age-dependent
|
||||
* form fields. When no birth date is provided, age-restricted fields should be
|
||||
* hidden until this prerequisite is met.
|
||||
*
|
||||
* The condition is commonly used to control field visibility, ensuring that
|
||||
* age-dependent services and options are only displayed when the participant's
|
||||
* age can be calculated.
|
||||
*/
|
||||
class DateOfBirthProvidedCondition implements FieldConditionInterface
|
||||
{
|
||||
/**
|
||||
* Evaluates if the participant has provided their date of birth.
|
||||
*
|
||||
* Checks if the participant exists and has a non-null dateOfBirth property.
|
||||
* This is a prerequisite for showing age-dependent form fields and services.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data (unused for this condition)
|
||||
*
|
||||
* @return bool True if the participant has provided their date of birth, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
|
||||
return null !== $participant && null !== $participant->dateOfBirth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns field names that this condition depends on.
|
||||
*
|
||||
* The date of birth condition depends on the participant's dateOfBirth field.
|
||||
* When this field changes, any conditions based on birth date availability
|
||||
* should be re-evaluated.
|
||||
*
|
||||
* @return string[] Array containing 'dateOfBirth' field name
|
||||
*/
|
||||
public function getDependentFields(): array
|
||||
{
|
||||
return ['dateOfBirth'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable description of this condition.
|
||||
*
|
||||
* Provides a clear description of what this condition checks for,
|
||||
* useful for debugging and understanding field state logic.
|
||||
*
|
||||
* @return string Description of the date of birth requirement
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Participant must provide their date of birth';
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
||||
use App\Form\Service\Condition\CompositeCondition;
|
||||
use App\Form\Service\Condition\DateOfBirthProvidedCondition;
|
||||
use App\Form\Service\Condition\RentalSelectionCondition;
|
||||
|
||||
/**
|
||||
@@ -14,8 +16,9 @@ use App\Form\Service\Condition\RentalSelectionCondition;
|
||||
* 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.
|
||||
* Current field state conditions:
|
||||
* - Body dimension fields become required when rental services are selected
|
||||
* - Age-dependent service fields are hidden until birth date is provided
|
||||
*/
|
||||
class CreateFieldStateProvider extends AbstractFieldStateProvider
|
||||
{
|
||||
@@ -60,6 +63,26 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
||||
'required' => $rentalCondition,
|
||||
];
|
||||
|
||||
// Hide age-dependent fields when no date of birth is provided
|
||||
$dateOfBirthProvidedCondition = new DateOfBirthProvidedCondition();
|
||||
|
||||
// Age-dependent service fields are hidden until birth date is provided
|
||||
$this->fieldStateConditions['courses'] = [
|
||||
'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
];
|
||||
|
||||
$this->fieldStateConditions['additionalServices'] = [
|
||||
'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
];
|
||||
|
||||
$this->fieldStateConditions['rentals'] = [
|
||||
'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
];
|
||||
|
||||
$this->fieldStateConditions['board'] = [
|
||||
'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
];
|
||||
|
||||
// Example field state conditions would be registered here
|
||||
// For demonstration purposes, here are some example patterns:
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
|
||||
use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory;
|
||||
use App\Form\Service\ServiceAgeEvaluator;
|
||||
|
||||
/**
|
||||
* Provides dynamic field options for participant form fields.
|
||||
@@ -40,9 +41,8 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
*
|
||||
* @param ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory Factory for creating room choice loaders
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory,
|
||||
) {
|
||||
public function __construct(private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -86,23 +86,31 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
: null,
|
||||
];
|
||||
|
||||
// Courses field provider - provides available courses from travel data
|
||||
$this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto) => [
|
||||
// Courses field provider - provides age-appropriate courses from travel data
|
||||
$this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
|
||||
'label' => 'Kurse',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES),
|
||||
'choices' => $this->filterServicesByAgeConstraints(
|
||||
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES),
|
||||
$bookingDto,
|
||||
$participantIndex
|
||||
),
|
||||
'choice_label' => 'label',
|
||||
];
|
||||
|
||||
// Additional services field provider - provides additional services with mandatory pre-selection
|
||||
$this->fieldOptionProviders['additionalServices'] = fn (BookingDtoInterface $bookingDto) => [
|
||||
// Additional services field provider - provides age-appropriate additional services with mandatory pre-selection
|
||||
$this->fieldOptionProviders['additionalServices'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
|
||||
'label' => 'Zusatzleistungen',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL),
|
||||
'choices' => $this->filterServicesByAgeConstraints(
|
||||
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL),
|
||||
$bookingDto,
|
||||
$participantIndex
|
||||
),
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => 'label',
|
||||
'choice_attr' => function (?Service $service) {
|
||||
@@ -124,23 +132,31 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
},
|
||||
];
|
||||
|
||||
// Board field provider - provides available board options from travel data
|
||||
$this->fieldOptionProviders['board'] = fn (BookingDtoInterface $bookingDto) => [
|
||||
// Board field provider - provides age-appropriate board options from travel data
|
||||
$this->fieldOptionProviders['board'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
|
||||
'label' => 'Verpflegung',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD),
|
||||
'choices' => $this->filterServicesByAgeConstraints(
|
||||
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD),
|
||||
$bookingDto,
|
||||
$participantIndex
|
||||
),
|
||||
'choice_label' => 'label',
|
||||
];
|
||||
|
||||
// Rentals field provider - provides available rental options from travel data filtered by date range
|
||||
$this->fieldOptionProviders['rentals'] = fn (BookingDtoInterface $bookingDto) => [
|
||||
// Rentals field provider - provides age-appropriate rental options from travel data filtered by date range
|
||||
$this->fieldOptionProviders['rentals'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
|
||||
'label' => 'Leihmaterial',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true, true),
|
||||
'choices' => $this->filterServicesByAgeConstraints(
|
||||
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true, true),
|
||||
$bookingDto,
|
||||
$participantIndex
|
||||
),
|
||||
'choice_label' => 'label',
|
||||
];
|
||||
|
||||
@@ -155,4 +171,38 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
// ],
|
||||
// ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters services based on participant's age constraints.
|
||||
*
|
||||
* Removes services that have age restrictions the participant doesn't meet.
|
||||
* If no age evaluator is configured or participant has no birth date,
|
||||
* returns empty array to be handled by field visibility conditions.
|
||||
*
|
||||
* @param array $services Services to filter
|
||||
* @param BookingDtoInterface $bookingDto Booking data containing participant info
|
||||
* @param int $participantIndex Index of participant to evaluate
|
||||
*
|
||||
* @return array Filtered services array
|
||||
*/
|
||||
private function filterServicesByAgeConstraints(array $services, BookingDtoInterface $bookingDto, int $participantIndex): array
|
||||
{
|
||||
$ageEvaluator = new ServiceAgeEvaluator();
|
||||
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
|
||||
// If no birth date provided, return empty array (handled by field visibility conditions)
|
||||
if (null === $participant || null === $participant->dateOfBirth) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter($services, function (Service $service) use ($bookingDto, $participantIndex, $ageEvaluator) {
|
||||
// No age constraints = available to all
|
||||
if (false === $ageEvaluator->canEvaluate($service)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
|
||||
/**
|
||||
* Evaluates service availability based on participant age constraints.
|
||||
*
|
||||
* This service handles the business logic of determining whether a service
|
||||
* is available to a specific participant based on their age or birth year.
|
||||
* It supports both absolute age constraints and birth year range constraints.
|
||||
*/
|
||||
class ServiceAgeEvaluator
|
||||
{
|
||||
/**
|
||||
* Determines if this evaluator can process the given service.
|
||||
*
|
||||
* @param Service $service The service to evaluate
|
||||
*
|
||||
* @return bool True if the service has age constraints that can be evaluated
|
||||
*/
|
||||
public function canEvaluate(Service $service): bool
|
||||
{
|
||||
return null !== $service->ageConstraintType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates if a service is available for a specific participant.
|
||||
*
|
||||
* Checks the participant's age or birth year against the service's
|
||||
* constraint requirements. Returns false if the participant doesn't
|
||||
* meet the age requirements or if no birth date is provided.
|
||||
*
|
||||
* @param Service $service The service to evaluate
|
||||
* @param BookingDtoInterface $bookingDto The booking containing participant data
|
||||
* @param int $participantIndex The index of the participant to evaluate
|
||||
*
|
||||
* @return bool True if the service is available for the participant
|
||||
*/
|
||||
public function isServiceAvailableForParticipant(Service $service, BookingDtoInterface $bookingDto, int $participantIndex): bool
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
|
||||
if (null === $participant || null === $participant->dateOfBirth) {
|
||||
return false; // Cannot evaluate without birth date
|
||||
}
|
||||
|
||||
return match($service->ageConstraintType) {
|
||||
'absolute_age' => $this->evaluateAbsoluteAge($service, $participant->dateOfBirth),
|
||||
'birth_year' => $this->evaluateBirthYear($service, $participant->dateOfBirth),
|
||||
'mixed' => $this->evaluateAbsoluteAge($service, $participant->dateOfBirth)
|
||||
&& $this->evaluateBirthYear($service, $participant->dateOfBirth),
|
||||
default => true // No constraints or unknown type
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates absolute age constraints against participant's current age.
|
||||
*
|
||||
* @param Service $service The service with age constraints
|
||||
* @param \DateTimeImmutable $dateOfBirth The participant's date of birth
|
||||
*
|
||||
* @return bool True if the participant meets the absolute age requirements
|
||||
*/
|
||||
private function evaluateAbsoluteAge(Service $service, \DateTimeImmutable $dateOfBirth): bool
|
||||
{
|
||||
$age = $this->calculateAge($dateOfBirth);
|
||||
|
||||
if (null !== $service->ageFrom && $age < $service->ageFrom) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null !== $service->ageTo && $age > $service->ageTo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates birth year constraints against participant's birth year.
|
||||
*
|
||||
* @param Service $service The service with birth year constraints
|
||||
* @param \DateTimeImmutable $dateOfBirth The participant's date of birth
|
||||
*
|
||||
* @return bool True if the participant meets the birth year requirements
|
||||
*/
|
||||
private function evaluateBirthYear(Service $service, \DateTimeImmutable $dateOfBirth): bool
|
||||
{
|
||||
$birthYear = (int) $dateOfBirth->format('Y');
|
||||
|
||||
if (null !== $service->birthYearFrom && $birthYear < $service->birthYearFrom) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null !== $service->birthYearTo && $birthYear > $service->birthYearTo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates age in years from a date of birth.
|
||||
*
|
||||
* Uses DateTimeImmutable to ensure accurate age calculations accounting
|
||||
* for leap years and exact birth date anniversaries.
|
||||
*
|
||||
* @param \DateTimeImmutable $dateOfBirth The participant's date of birth
|
||||
*
|
||||
* @return int The calculated age in complete years
|
||||
*/
|
||||
private function calculateAge(\DateTimeImmutable $dateOfBirth): int
|
||||
{
|
||||
$today = new \DateTimeImmutable();
|
||||
|
||||
return (int) $dateOfBirth->diff($today)->y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a human-readable description of the service's age constraints.
|
||||
*
|
||||
* Generates descriptive text explaining the age requirements,
|
||||
* useful for user interfaces and debugging.
|
||||
*
|
||||
* @param Service $service The service to describe
|
||||
*
|
||||
* @return string Description of the age constraints
|
||||
*/
|
||||
public function getConstraintDescription(Service $service): string
|
||||
{
|
||||
return match($service->ageConstraintType) {
|
||||
'absolute_age' => $this->getAbsoluteAgeDescription($service),
|
||||
'birth_year' => $this->getBirthYearDescription($service),
|
||||
'mixed' => sprintf('%s and %s',
|
||||
$this->getAbsoluteAgeDescription($service),
|
||||
$this->getBirthYearDescription($service)),
|
||||
default => 'No age restrictions'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets description for absolute age constraints.
|
||||
*
|
||||
* @param Service $service The service with age constraints
|
||||
*
|
||||
* @return string Description of absolute age requirements
|
||||
*/
|
||||
private function getAbsoluteAgeDescription(Service $service): string
|
||||
{
|
||||
if (null !== $service->ageFrom && null !== $service->ageTo) {
|
||||
return sprintf('Ages %d-%d', $service->ageFrom, $service->ageTo);
|
||||
}
|
||||
|
||||
if (null !== $service->ageFrom) {
|
||||
return sprintf('Age %d+', $service->ageFrom);
|
||||
}
|
||||
|
||||
if (null !== $service->ageTo) {
|
||||
return sprintf('Age up to %d', $service->ageTo);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets description for birth year constraints.
|
||||
*
|
||||
* @param Service $service The service with birth year constraints
|
||||
*
|
||||
* @return string Description of birth year requirements
|
||||
*/
|
||||
private function getBirthYearDescription(Service $service): string
|
||||
{
|
||||
if (null !== $service->birthYearFrom && null !== $service->birthYearTo) {
|
||||
if ($service->birthYearFrom === $service->birthYearTo) {
|
||||
return sprintf('Born in %d', $service->birthYearFrom);
|
||||
}
|
||||
return sprintf('Born %d-%d', $service->birthYearFrom, $service->birthYearTo);
|
||||
}
|
||||
|
||||
if (null !== $service->birthYearFrom) {
|
||||
return sprintf('Born %d or later', $service->birthYearFrom);
|
||||
}
|
||||
|
||||
if (null !== $service->birthYearTo) {
|
||||
return sprintf('Born up to %d', $service->birthYearTo);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user