231 lines
9.4 KiB
PHP
231 lines
9.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\BusProNet\XmlParser;
|
|
|
|
use App\BusProNet\Model\Insurance;
|
|
use Symfony\Component\DomCrawler\Crawler;
|
|
|
|
/**
|
|
* Parses insurance XML data into Insurance model objects.
|
|
*
|
|
* Handles both individual insurance products (versicherung) and insurance packages
|
|
* (versicherungspaket) with attribute-based parsing, type conversion, and filtering logic.
|
|
*/
|
|
class InsuranceParser extends AbstractParser
|
|
{
|
|
/**
|
|
* Parses insurance XML nodes into Insurance objects.
|
|
*
|
|
* @param Crawler $xmlContent The XML crawler containing insurance data
|
|
*
|
|
* @return array<string|int, Insurance> 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) {
|
|
$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<int, Insurance> $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<int, Insurance> $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 = $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<int> 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[] = $refNode->attr('idbuspro');
|
|
});
|
|
});
|
|
|
|
return array_unique($referencedIds);
|
|
}
|
|
|
|
/**
|
|
* Parses contained insurance IDs from a package node.
|
|
*
|
|
* @param Crawler $node The package XML node
|
|
*
|
|
* @return array<int> 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[] = $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);
|
|
}
|
|
}
|