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); // Second pass: Parse individual insurances with conditional filtering $individualInsurances = []; $xmlContent->filterXPath('//versicherungen/versicherung') ->each(function (Crawler $node) use (&$individualInsurances, &$insurances) { $id = (int) $node->attr('idbuspro'); // Individual insurances have int IDs $isComplementary = $this->getBoolAttributeValue($node->attr('zusatzversicherung')); // Parse all individual insurances for reference lookup $insurance = $this->parseInsuranceNode($node, false); if (null !== $insurance && null !== $insurance->id) { // Set complementary flag from XML attribute $insurance->complementary = $isComplementary; $individualInsurances[$insurance->id] = $insurance; // Include ALL insurances in the result array // Complementary insurances will be filtered out at the form field level $insurances[$insurance->id] = $insurance; } }); // Third pass: Parse packages with family detection based on contained insurances $xmlContent->filterXPath('//versicherungspakete/versicherungspaket') ->each(function (Crawler $node) use (&$insurances, $individualInsurances) { $insurance = $this->parseInsuranceNode($node, true, $individualInsurances); if (null !== $insurance && null !== $insurance->id) { // Parse contained insurance IDs $insurance->containedInsuranceIds = $this->parseContainedInsuranceIds($node); // Populate containedInsurances with actual Insurance objects $insurance->containedInsurances = []; foreach ($insurance->containedInsuranceIds as $containedId) { if (isset($individualInsurances[$containedId])) { $insurance->containedInsurances[] = $individualInsurances[$containedId]; } } $insurances[$insurance->id] = $insurance; } }); return $insurances; } /** * Determines if a package contains any family insurances. * * @param Crawler $packageNode The package XML node * @param array $individualInsurances Parsed individual insurances for reference * * @return bool True if any contained insurance is a family insurance */ private function isPackageFamilyInsurance(Crawler $packageNode, array $individualInsurances): bool { // Check if any contained insurance is a family insurance $containedInsuranceIds = $this->parseContainedInsuranceIds($packageNode); foreach ($containedInsuranceIds as $containedId) { if (isset($individualInsurances[$containedId]) && true === $individualInsurances[$containedId]->familyInsurance) { return true; } } return false; } /** * Parses a single insurance or package node. * * @param Crawler $node The XML node to parse * @param bool $isPackage Whether this is a package node * @param array $individualInsurances Individual insurances for package family detection * * @return Insurance|null The parsed insurance object */ private function parseInsuranceNode(Crawler $node, bool $isPackage, array $individualInsurances = []): ?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 = $this->normalizeInsuranceLabel($node->attr('bezeichnung')); $insurance->subType = $node->attr('unterart'); $insurance->price = $node->attr('preis') ? $this->stringToFloat($node->attr('preis')) : null; $insurance->familyInsurance = $this->stringToBool($node->attr('familienversicherung')); // For packages, check if any contained insurances are family insurances if ($isPackage && !$insurance->familyInsurance && !empty($individualInsurances)) { $insurance->familyInsurance = $this->isPackageFamilyInsurance($node, $individualInsurances); } // 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; } /** * Normalizes insurance labels by removing redundant phrases and replacing abbreviations. * * Transformations: * - Removes "Auto/Bahn/Bus (Europa)" - redundant travel mode specification * - Replaces "FAM" with "Familie" - clearer German terminology * * @param string|null $label The original insurance label from XML * * @return string|null The normalized label, or null if input was null */ private function normalizeInsuranceLabel(?string $label): ?string { if (null === $label) { return null; } // Remove redundant travel mode specification $label = str_replace('Auto/Bahn/Bus (Europa)', '', $label); // Replace abbreviation with full German word $label = str_replace('FAM', 'Familie', $label); // Clean up extra spaces that may result from removals $label = preg_replace('/\s+/', ' ', $label); $label = trim($label); return $label; } private function getBoolAttributeValue(?string $value): bool { return !empty($value) && $this->stringToBool($value); } }