383 lines
15 KiB
PHP
383 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\BusProNet\XmlParser;
|
|
|
|
use App\BusProNet\Constants;
|
|
use App\BusProNet\Model\CrmSelection;
|
|
use App\BusProNet\Model\CrmSelectionGroup;
|
|
use App\BusProNet\Model\Guide;
|
|
use App\BusProNet\Model\Pickup;
|
|
use App\BusProNet\Model\Room;
|
|
use App\BusProNet\Model\Service;
|
|
use App\BusProNet\Model\Travel;
|
|
use App\BusProNet\Utility\DayTimeUtility;
|
|
use Symfony\Component\DomCrawler\Crawler;
|
|
|
|
/**
|
|
* Parser for travel XML data from BusProNet API or XML exports.
|
|
*
|
|
* This parser handles the extraction and parsing of travel information from XML
|
|
* including dates, services, rooms, pickups, and pricing. It can be used by both
|
|
* file-based loaders and API response parsers.
|
|
*/
|
|
class TravelParser extends AbstractParser
|
|
{
|
|
private AgeConstraintParserRegistry $ageConstraintRegistry;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->ageConstraintRegistry = new AgeConstraintParserRegistry();
|
|
}
|
|
|
|
/**
|
|
* Parse XML node into a Travel object.
|
|
*
|
|
* Extracts all travel-related data from the XML node including dates,
|
|
* pricing, services, rooms, pickups, and guide information.
|
|
*
|
|
* @param Crawler $node The XML node containing travel data
|
|
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
|
*
|
|
* @return Travel The parsed travel object
|
|
*/
|
|
public function parse(Crawler $node, ?int $hotelId = null): Travel
|
|
{
|
|
$hotelNode = $this->getHotelNode($node, $hotelId);
|
|
|
|
$dateFrom = $this->stringToDate($node->attr('termin'));
|
|
|
|
$travel = new Travel();
|
|
$travel->id = (int) $node->attr('idbuspro');
|
|
$travel->hotelId = (int) $hotelNode->attr('idbuspro');
|
|
$travel->label = $this->getStringOrNullValue($node->filterXPath('//text'));
|
|
$travel->dateFrom = $dateFrom;
|
|
$travel->dateTo = $this->stringToDate($node->attr('bis'));
|
|
$travel->code = $node->attr('code');
|
|
$travel->type = $node->attr('reiseart');
|
|
$travel->priceFrom = $this->stringToFloat($this->getStringOrNullValue($node->filterXPath('//abpreis')));
|
|
$travel->selectionGroups = $this->getSelectionGroups($node->filterXPath('//selektiongruppe'));
|
|
$travel->additionalServices = $this
|
|
->getAdditionalServices($node->filterXPath('//lei_sonstiges/leistung'));
|
|
$travel->transportationServices = $this
|
|
->getTransportationServices($node->filterXPath('//lei_befoerderung/leistung'));
|
|
$travel->rooms = $this->getRooms($hotelNode);
|
|
$travel->pickupsTo = $this->getPickups($node->filterXPath('//zustiege/zustieg'), $dateFrom);
|
|
$travel->pickupsFro = $this->getPickups($node->filterXPath('//zustiege_rueck/zustieg_rueck'));
|
|
$travel->guide = $this->getGuide($node);
|
|
|
|
return $travel;
|
|
}
|
|
|
|
/**
|
|
* Parse selection groups from XML node.
|
|
*
|
|
* Extracts CRM selection groups and their associated selections from
|
|
* the XML structure. Each group contains multiple selection options.
|
|
*
|
|
* @param Crawler $node The XML node containing selection group data
|
|
*
|
|
* @return array<int, CrmSelectionGroup> Array of selection groups indexed by ID
|
|
*/
|
|
public function getSelectionGroups(Crawler $node): array
|
|
{
|
|
$selectionGroups = [];
|
|
|
|
$node->each(function (Crawler $groupNode) use (&$selectionGroups) {
|
|
$groupId = (int) $groupNode->attr('idbuspro');
|
|
$selectionGroup = new CrmSelectionGroup();
|
|
$selectionGroup->id = $groupId;
|
|
$selectionGroup->label = $groupNode->attr('bezeichnung');
|
|
|
|
$groupNode
|
|
->filterXPath('//selektion')
|
|
->each(function (Crawler $selectionNode) use (&$selectionGroups, &$selectionGroup, $groupId) {
|
|
$selectionId = (int) $selectionNode->attr('idbuspro');
|
|
$selection = new CrmSelection();
|
|
$selection->id = $selectionId;
|
|
$selection->label = $selectionNode->attr('bezeichnung');
|
|
$selectionGroups[$groupId]['selections'][$selectionId] = $selectionNode->attr('bezeichnung');
|
|
$selectionGroup->selections[] = $selection;
|
|
})
|
|
;
|
|
|
|
$selectionGroups[$groupId] = $selectionGroup;
|
|
});
|
|
|
|
return $selectionGroups;
|
|
}
|
|
|
|
/**
|
|
* Parse additional services from XML node.
|
|
*
|
|
* Extracts additional services like insurance, activities, or extras
|
|
* from the XML structure with pricing and availability information.
|
|
*
|
|
* @param Crawler $node The XML node containing additional service data
|
|
*
|
|
* @return array<int, Service> Array of additional services indexed by ID
|
|
*/
|
|
public function getAdditionalServices(Crawler $node): array
|
|
{
|
|
$additionalServices = [];
|
|
|
|
$node->each(function (Crawler $serviceNode) use (&$additionalServices) {
|
|
$serviceId = (int) $serviceNode->attr('idbuspro');
|
|
|
|
$service = new Service();
|
|
$service->source = Constants::SOURCE_TRAVEL;
|
|
$service->category = Constants::CATEGORY_ADDITIONAL;
|
|
$service->id = $serviceId;
|
|
$service->subType = $serviceNode->attr('unterart');
|
|
$service->mandatory = $this->stringToBool($serviceNode->attr('pflicht'));
|
|
$service->dateFrom = $this->stringToDate($serviceNode->attr('termin'));
|
|
$service->dateTo = $this->stringToDate($serviceNode->attr('bis'));
|
|
$service->label = $this->getStringOrNullValue($serviceNode->filterXPath('//text'));
|
|
$service->price = $this
|
|
->stringToFloat($this->getStringOrNullValue($serviceNode->filterXPath('//preis')));
|
|
$service->status = $this->getStringOrNullValue($serviceNode->filterXPath('//status'));
|
|
|
|
// Parse optional description from hinweis node
|
|
$description = $this->getStringOrNullValue($serviceNode->filterXPath('//hinweis'));
|
|
if (null !== $description && '' !== trim($description)) {
|
|
$service->description = $description;
|
|
}
|
|
|
|
// Parse age constraints
|
|
$this->parseServiceAgeConstraints($serviceNode, $service);
|
|
|
|
$additionalServices[$serviceId] = $service;
|
|
});
|
|
|
|
return $additionalServices;
|
|
}
|
|
|
|
/**
|
|
* Parse transportation services from XML node.
|
|
*
|
|
* Extracts transportation services like bus, train, or flight options
|
|
* with scheduling, pricing, and direction information.
|
|
*
|
|
* @param Crawler $node The XML node containing transportation service data
|
|
*
|
|
* @return array<int, Service> Array of transportation services indexed by ID
|
|
*/
|
|
public function getTransportationServices(Crawler $node): array
|
|
{
|
|
$transportationServices = [];
|
|
|
|
$node->each(function (Crawler $serviceNode) use (&$transportationServices) {
|
|
$serviceId = (int) $serviceNode->attr('idbuspro');
|
|
|
|
$service = new Service();
|
|
$service->source = Constants::SOURCE_TRAVEL;
|
|
$service->category = Constants::CATEGORY_TRANSPORTATION;
|
|
$service->id = $serviceId;
|
|
$service->subType = $serviceNode->attr('unterart');
|
|
$service->dateFrom = $this->stringToDate($serviceNode->attr('termin'));
|
|
$service->dateTo = $this->stringToDate($serviceNode->attr('bis'));
|
|
$service->label = $this->getStringOrNullValue($serviceNode->filterXPath('//text'));
|
|
$service->direction = $this->getStringOrNullValue($serviceNode->filterXPath('//richtung'));
|
|
$service->status = $this->getStringOrNullValue($serviceNode->filterXPath('//status'));
|
|
|
|
// Parse optional description from hinweis node
|
|
$description = $this->getStringOrNullValue($serviceNode->filterXPath('//hinweis'));
|
|
if (null !== $description && '' !== trim($description)) {
|
|
$service->description = $description;
|
|
}
|
|
|
|
if (null !== $timeFrom = $serviceNode->attr('uhrzeit_von')) {
|
|
$service->timeFrom = $timeFrom;
|
|
$service->dayTime = (new DayTimeUtility())->mapTime($timeFrom);
|
|
}
|
|
|
|
$transportationServices[$serviceId] = $service;
|
|
});
|
|
|
|
return $transportationServices;
|
|
}
|
|
|
|
/**
|
|
* Extract guide information from travel XML node.
|
|
*
|
|
* Searches for guide information in bus transportation services
|
|
* and extracts name and phone contact details.
|
|
*
|
|
* @param Crawler $travelNode The XML node containing travel data
|
|
*
|
|
* @return Guide|null The guide object or null if no guide found
|
|
*/
|
|
public function getGuide(Crawler $travelNode): ?Guide
|
|
{
|
|
$guideNodes = $travelNode
|
|
->filterXPath('//lei_befoerderung/leistung[@unterart="BUS"]/zustiegsplanung/reiseleiter');
|
|
|
|
if (0 < $guideNodes->count()) {
|
|
$guideNode = $guideNodes->first();
|
|
|
|
$guide = new Guide();
|
|
$guide->name = $this->getStringOrNullValue($guideNode->filterXPath('//name'));
|
|
$guide->phone = $this->getStringOrNullValue($guideNode->filterXPath('//telefon'));
|
|
|
|
return $guide;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Parse pickup locations from XML node.
|
|
*
|
|
* Extracts pickup/drop-off locations with pricing and timing information.
|
|
* Date and time parsing is only performed for outbound journeys when
|
|
* a default date is provided.
|
|
*
|
|
* @param Crawler $node The XML node containing pickup data
|
|
* @param \DateTimeImmutable|null $defaultDate Default date for time parsing (outbound only)
|
|
*
|
|
* @return array<int, Pickup> Array of pickup locations indexed by ID
|
|
*/
|
|
public function getPickups(Crawler $node, ?\DateTimeImmutable $defaultDate = null): array
|
|
{
|
|
$pickups = [];
|
|
|
|
$node->each(function (Crawler $pickupNode) use (&$pickups, $defaultDate) {
|
|
$pickupId = (int) $pickupNode->attr('idbuspro');
|
|
|
|
$pickup = new Pickup();
|
|
$pickup->id = $pickupId;
|
|
$pickup->price = $pickupNode->attr('preis') ?
|
|
$this->stringToFloat($pickupNode->attr('preis')) : null;
|
|
|
|
// parse date and time only for direction 'to' indicated by provided default date
|
|
if (null !== $defaultDate) {
|
|
$pickup->time = $this->stringToDateTimeFuzzy($pickupNode->attr('zeit'), $defaultDate);
|
|
}
|
|
|
|
$pickups[$pickupId] = $pickup;
|
|
});
|
|
|
|
return $pickups;
|
|
}
|
|
|
|
/**
|
|
* Get the hotel node from the travel XML structure.
|
|
*
|
|
* Retrieves the hotel node either by specific hotel ID or returns
|
|
* the first hotel node if no ID is specified.
|
|
*
|
|
* @param Crawler $node The XML node containing hotel data
|
|
* @param int|null $hotelId Optional hotel ID to filter by
|
|
*
|
|
* @return Crawler The hotel XML node
|
|
*/
|
|
public function getHotelNode(Crawler $node, ?int $hotelId): Crawler
|
|
{
|
|
// In case not hotel id is provided, take the first hotel node (which is most probably the only one)
|
|
if (null === $hotelId) {
|
|
return $node->filterXPath('//hotel')->first();
|
|
}
|
|
|
|
$hotelNode = $node->filterXPath(sprintf('//hotel[@idbuspro="%d"]', $hotelId));
|
|
|
|
return $hotelNode->first();
|
|
}
|
|
|
|
/**
|
|
* Parse room information from hotel XML node.
|
|
*
|
|
* Extracts room details including pricing, capacity, availability,
|
|
* and board options from the hotel XML structure.
|
|
*
|
|
* @param Crawler $node The XML node containing room data
|
|
*
|
|
* @return array<int, Room> Array of rooms indexed by room ID
|
|
*/
|
|
public function getRooms(Crawler $node): array
|
|
{
|
|
$roomNodes = $node->filterXPath('//zimmer/preis');
|
|
|
|
if (0 === $roomNodes->count()) {
|
|
return [];
|
|
}
|
|
|
|
$rooms = [];
|
|
|
|
$roomNodes->each(function (Crawler $roomNode) use (&$rooms) {
|
|
$roomId = (int) $roomNode->attr('idbuspro_zimmer');
|
|
|
|
$room = new Room();
|
|
$room->id = $roomId;
|
|
$room->code = $roomNode->attr('zimmercode');
|
|
$room->category = $roomNode->attr('kat');
|
|
$room->boardId = (int) $roomNode->attr('idbuspro_vp');
|
|
$room->label = $roomNode->attr('zimmertext');
|
|
$room->minPax = (int) $roomNode->attr('minpax');
|
|
$room->maxPax = (int) $roomNode->attr('maxpax');
|
|
$room->nights = (int) $roomNode->attr('naechte');
|
|
$room->price = $roomNode->attr('preis') ?
|
|
$this->stringToFloat($roomNode->attr('preis')) : null;
|
|
$room->status = $roomNode->attr('status');
|
|
$room->available = (int) $roomNode->attr('verfuegbar');
|
|
|
|
$rooms[$roomId] = $room;
|
|
});
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|