From 9d44197246e9e7da8887e80b55841e939796c348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Wed, 24 Sep 2025 15:28:43 +0200 Subject: [PATCH] feat: parser and loader for insurance data --- src/BusProNet/Model/Insurance.php | 91 ++++++++++++ src/BusProNet/XmlLoader/InsuranceLoader.php | 80 ++++++++++ src/BusProNet/XmlParser/InsuranceParser.php | 154 ++++++++++++++++++++ 3 files changed, 325 insertions(+) create mode 100644 src/BusProNet/Model/Insurance.php create mode 100644 src/BusProNet/XmlLoader/InsuranceLoader.php create mode 100644 src/BusProNet/XmlParser/InsuranceParser.php diff --git a/src/BusProNet/Model/Insurance.php b/src/BusProNet/Model/Insurance.php new file mode 100644 index 0000000..bb5d44c --- /dev/null +++ b/src/BusProNet/Model/Insurance.php @@ -0,0 +1,91 @@ + 'Y-m-d'])] + public ?\DateTimeImmutable $travelDateFrom = null; + + #[Groups(['api:single', 'api:list'])] + #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] + public ?\DateTimeImmutable $travelDateTo = null; + + #[Groups(['api:single', 'api:list'])] + #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] + public ?\DateTimeImmutable $bookingDateFrom = null; + + #[Groups(['api:single', 'api:list'])] + #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] + public ?\DateTimeImmutable $bookingDateTo = 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 ?float $travelPriceFrom = null; + + #[Groups(['api:single', 'api:list'])] + public ?float $travelPriceTo = null; + + #[Groups(['api:single', 'api:list'])] + public ?int $travelDurationFrom = null; + + #[Groups(['api:single', 'api:list'])] + public ?int $travelDurationTo = null; + + #[Groups(['api:single', 'api:list'])] + public ?string $urlInfo = null; + + #[Groups(['api:single', 'api:list'])] + public ?string $urlProductInfo = null; + + #[Groups(['api:single', 'api:list'])] + public ?string $urlTerms = null; + + /** + * @var array Insurance products contained in this package (only for packages) + */ + #[Groups(['api:single', 'api:list'])] + public array $containedInsurances = []; + + /** + * @var array IDs of insurance products contained in this package (only for packages) + */ + #[Groups(['api:single', 'api:list'])] + public array $containedInsuranceIds = []; +} \ No newline at end of file diff --git a/src/BusProNet/XmlLoader/InsuranceLoader.php b/src/BusProNet/XmlLoader/InsuranceLoader.php new file mode 100644 index 0000000..1ba18a6 --- /dev/null +++ b/src/BusProNet/XmlLoader/InsuranceLoader.php @@ -0,0 +1,80 @@ +cache->get('bpn_insurances', function (ItemInterface $item) use ($filename) { + $item->expiresAfter(3 * 60 * 60); + + $crawler = $this->loadXml($filename); + + return $this->insuranceParser->parse($crawler); + }); + } catch (InvalidArgumentException $e) { + return []; + } + } + + public function loadById(string|int $id, ?string $filename = 'versicherungen.xml'): ?Insurance + { + $insurances = $this->loadAll($filename); + + return $insurances[$id] ?? null; + } + + /** + * Loads all insurances with resolved package relationships. + * + * @param string|null $filename The XML filename to load from + * @return array Array of Insurance objects with resolved relationships + */ + public function loadAllWithRelationships(?string $filename = 'versicherungen.xml'): array + { + $insurances = $this->loadAll($filename); + + // Resolve package relationships + foreach ($insurances as $insurance) { + if ($insurance->package && !empty($insurance->containedInsuranceIds)) { + foreach ($insurance->containedInsuranceIds as $containedId) { + if (isset($insurances[$containedId])) { + $insurance->containedInsurances[] = $insurances[$containedId]; + } else { + // Log missing insurance reference for debugging + error_log("Insurance package {$insurance->id} references missing insurance {$containedId}"); + } + } + } + } + + return $insurances; + } + + private function loadXml(?string $filename = 'versicherungen.xml'): Crawler + { + $xml = $this->xmlExport->read($filename); + + return new Crawler($xml); + } +} diff --git a/src/BusProNet/XmlParser/InsuranceParser.php b/src/BusProNet/XmlParser/InsuranceParser.php new file mode 100644 index 0000000..0690db0 --- /dev/null +++ b/src/BusProNet/XmlParser/InsuranceParser.php @@ -0,0 +1,154 @@ + Array of Insurance objects indexed by id + */ + public function parse(Crawler $xmlContent): array + { + $insurances = []; + + // First pass: collect referenced insurance IDs from packages + $referencedIds = $this->collectReferencedInsuranceIds($xmlContent); + + // Parse individual insurances with conditional filtering + $xmlContent->filterXPath('//versicherungen/versicherung') + ->each(function (Crawler $node) use (&$insurances, $referencedIds) { + $id = (int) $node->attr('idbuspro'); // Individual insurances have int IDs + $isZusatz = $this->getBoolAttributeValue($node->attr('zusatzversicherung')); + + // Include if: not zusatzversicherung OR referenced by package + if (!$isZusatz || in_array($id, $referencedIds, true)) { + $insurance = $this->parseInsuranceNode($node, false); + if (null !== $insurance && null !== $insurance->id) { + $insurances[$insurance->id] = $insurance; + } + } + }); + + // Parse packages (no filtering needed) + $xmlContent->filterXPath('//versicherungspakete/versicherungspaket') + ->each(function (Crawler $node) use (&$insurances) { + $insurance = $this->parseInsuranceNode($node, true); + if (null !== $insurance && null !== $insurance->id) { + // Parse contained insurance IDs + $insurance->containedInsuranceIds = $this->parseContainedInsuranceIds($node); + $insurances[$insurance->id] = $insurance; + } + }); + + return $insurances; + } + + /** + * Parses a single insurance or package node. + * + * @param Crawler $node The XML node to parse + * @param bool $isPackage Whether this is a package node + * @return Insurance|null The parsed insurance object + */ + private function parseInsuranceNode(Crawler $node, bool $isPackage): ?Insurance + { + $insurance = new Insurance(); + $insurance->package = $isPackage; + + // Basic identifiers - packages have string IDs with 'P' prefix, individual insurances have int IDs + $idValue = $node->attr('idbuspro'); + $insurance->id = $isPackage ? $idValue : (int) $idValue; + $insurance->code = $node->attr('code'); + $insurance->label = $node->attr('bezeichnung'); + $insurance->price = $node->attr('preis') ? + $this->stringToFloat($node->attr('preis')) : null; + $insurance->familyInsurance = $this->stringToBool($node->attr('familienversicherung')); + + // Date constraints + $insurance->travelDateFrom = $node->attr('reisedatumvon') ? + $this->stringToDate($node->attr('reisedatumvon')) : null; + $insurance->travelDateTo = $node->attr('reisedatumbis') ? + $this->stringToDate($node->attr('reisedatumbis')) : null; + $insurance->bookingDateFrom = $node->attr('buchungdatumvon') ? + $this->stringToDate($node->attr('buchungdatumvon')) : null; + $insurance->bookingDateTo = $node->attr('buchungdatumbis') ? + $this->stringToDate($node->attr('buchungdatumbis')) : null; + + // Age constraints + $insurance->ageFrom = $node->attr('altervon') ? (int) $node->attr('altervon') : null; + $insurance->ageTo = $node->attr('alterbis') ? (int) $node->attr('alterbis') : null; + + // Price constraints + $insurance->travelPriceFrom = $node->attr('reisepreisvon') ? + $this->stringToFloat($node->attr('reisepreisvon')) : null; + $insurance->travelPriceTo = $node->attr('reisepreisbis') ? + $this->stringToFloat($node->attr('reisepreisbis')) : null; + + // Duration constraints + $insurance->travelDurationFrom = $node->attr('reisedauervon') ? (int) $node->attr('reisedauervon') : null; + $insurance->travelDurationTo = $node->attr('reisedauerbis') ? (int) $node->attr('reisedauerbis') : null; + + // Information URLs + $insurance->urlInfo = $node->attr('urlinfo'); + $insurance->urlProductInfo = $node->attr('urlproduktinfo'); + $insurance->urlTerms = $node->attr('urlagb'); + + return $insurance; + } + + /** + * Collects all insurance IDs referenced by packages. + * + * @param Crawler $xmlContent The XML content to scan + * @return array Array of referenced insurance IDs + */ + private function collectReferencedInsuranceIds(Crawler $xmlContent): array + { + $referencedIds = []; + $xmlContent->filterXPath('//versicherungspakete/versicherungspaket') + ->each(function (Crawler $packageNode) use (&$referencedIds) { + $packageNode->filterXPath('.//enthalteneversicherung') + ->each(function (Crawler $refNode) use (&$referencedIds) { + $referencedIds[] = (int) $refNode->attr('idbuspro'); // Referenced insurances are always int IDs + }); + }); + + return array_unique($referencedIds); + } + + /** + * Parses contained insurance IDs from a package node. + * + * @param Crawler $node The package XML node + * @return array Array of contained insurance IDs (always int, as they reference individual insurances) + */ + private function parseContainedInsuranceIds(Crawler $node): array + { + $ids = []; + $node->filterXPath('.//enthalteneversicherungen/enthalteneversicherung') + ->each(function (Crawler $insuranceNode) use (&$ids) { + $ids[] = (int) $insuranceNode->attr('idbuspro'); // Individual insurance IDs are always int + }); + + return $ids; + } + + private function getBoolAttributeValue(?string $value): bool + { + return !empty($value) && $this->stringToBool($value); + } +} \ No newline at end of file