feat: parser and loader for insurance data
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
use Symfony\Component\Serializer\Attribute\Context;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
|
||||
/**
|
||||
* Represents an insurance product or package with pricing, eligibility, and booking constraints.
|
||||
*
|
||||
* This class handles both individual insurance products and insurance packages,
|
||||
* including pricing tiers, age restrictions, travel date validity, booking windows, and coverage details.
|
||||
*/
|
||||
class Insurance
|
||||
{
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public string|int|null $id = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $code = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $label = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?float $price = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public bool $familyInsurance = false;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public bool $package = false;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => '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> Insurance products contained in this package (only for packages)
|
||||
*/
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public array $containedInsurances = [];
|
||||
|
||||
/**
|
||||
* @var array<string|int> IDs of insurance products contained in this package (only for packages)
|
||||
*/
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public array $containedInsuranceIds = [];
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlLoader;
|
||||
|
||||
use App\BusProNet\Model\Insurance;
|
||||
use App\BusProNet\XmlParser\InsuranceParser;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
class InsuranceLoader extends AbstractLoader
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly CacheInterface $cache,
|
||||
protected readonly FilesystemOperator $xmlExport,
|
||||
private readonly InsuranceParser $insuranceParser,
|
||||
) {
|
||||
parent::__construct($cache, $xmlExport);
|
||||
}
|
||||
|
||||
public function loadAll(?string $filename = 'versicherungen.xml'): array
|
||||
{
|
||||
try {
|
||||
return $this->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<string|int, Insurance> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?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);
|
||||
|
||||
// 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<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[] = (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<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[] = (int) $insuranceNode->attr('idbuspro'); // Individual insurance IDs are always int
|
||||
});
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
private function getBoolAttributeValue(?string $value): bool
|
||||
{
|
||||
return !empty($value) && $this->stringToBool($value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user