wip: age based filtering of options

This commit is contained in:
Björn Fromme
2025-08-27 12:42:02 +02:00
parent 803b10f981
commit 25bbf3d44f
11 changed files with 751 additions and 27 deletions
@@ -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();
}
}
+21
View File
@@ -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';
}
}
+64
View File
@@ -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;
}
}
}