feat: unfiltered product export for the website's catalogue import
This commit is contained in:
1 parent
272c811f4f
commit
1d42565a84
11 files changed
+970
-33
No files matched your search
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
/**
|
||||
* Reads the parts of a `Ziel_*.xml` travel date that the product export carries, verbatim.
|
||||
*
|
||||
* Deliberately separate from TravelParser, which builds the booking path's models and
|
||||
* applies the booking path's rules: it drops the PDGS pseudo room, excludes the
|
||||
* hotel-nested services and levels boarding-point prices. The product export filters and
|
||||
* rewrites nothing -- its consumer applies its own rules -- so it reads the nodes directly
|
||||
* and returns plain arrays in source order.
|
||||
*
|
||||
* @see \App\Service\ProductExportBuilder
|
||||
*/
|
||||
class ProductExportParser extends AbstractParser
|
||||
{
|
||||
/**
|
||||
* Every selection group on the date, not only the `Leistungen *` ones.
|
||||
*
|
||||
* @return list<array{busProId: int, label: ?string, selections: list<array{busProId: int, label: ?string}>}>
|
||||
*/
|
||||
public function getSelectionGroups(Crawler $dateNode): array
|
||||
{
|
||||
return $this->children($dateNode, 'selektiongruppe')->each(fn (Crawler $group): array => [
|
||||
'busProId' => (int) $group->attr('idbuspro'),
|
||||
'label' => $group->attr('bezeichnung'),
|
||||
'selections' => $this->children($group, 'selektion')->each(fn (Crawler $selection): array => [
|
||||
'busProId' => (int) $selection->attr('idbuspro'),
|
||||
'label' => $selection->attr('bezeichnung'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Services of one `lei_*` list, keyed by nothing -- the consumer keys them by `busProId`.
|
||||
*
|
||||
* The per-list `id` attribute is not read: it restarts at 1 in every list, so it
|
||||
* identifies nothing once a date-level and a hotel-level list are merged.
|
||||
*
|
||||
* @return list<array{busProId: int, subType: ?string, label: ?string, price: ?float, status: ?string, direction: ?string}>
|
||||
*/
|
||||
public function getServices(Crawler $serviceNodes): array
|
||||
{
|
||||
return $serviceNodes->each(fn (Crawler $service): array => [
|
||||
'busProId' => (int) $service->attr('idbuspro'),
|
||||
'subType' => $service->attr('unterart'),
|
||||
'label' => $this->getStringOrNullValue($this->children($service, 'text')),
|
||||
'price' => $this->getFloatOrNullValue($this->children($service, 'preis')),
|
||||
'status' => $this->getStringOrNullValue($this->children($service, 'status')),
|
||||
'direction' => $this->getStringOrNullValue($this->children($service, 'richtung')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The hotel's rooms, without the PDGS pseudo room.
|
||||
*
|
||||
* `available` is a count of rooms, not persons. Occupancy is read case-insensitively:
|
||||
* the travel export writes `MinPax`/`MaxPax`.
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function getRooms(Crawler $hotelNode): array
|
||||
{
|
||||
$rooms = [];
|
||||
|
||||
$this->children($hotelNode, 'zimmer/preis')->each(function (Crawler $room) use (&$rooms) {
|
||||
$code = $this->trimOrNull($this->getAttrOrNullValue($room, 'zimmercode'));
|
||||
|
||||
if (Constants::PSEUDO_PRICE_ROOM_CODE === $code) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rooms[] = [
|
||||
'busProId' => $this->getIntAttrOrNullValue($room, 'idbuspro_zimmer'),
|
||||
'code' => $code,
|
||||
'label' => $this->getAttrOrNullValue($room, 'zimmertext'),
|
||||
'category' => $this->getAttrOrNullValue($room, 'kat'),
|
||||
'minPax' => $this->getIntAttrOrNullValue($room, 'minpax'),
|
||||
'maxPax' => $this->getIntAttrOrNullValue($room, 'maxpax'),
|
||||
'nights' => $this->getIntAttrOrNullValue($room, 'naechte'),
|
||||
'price' => $this->stringToFloat($this->getAttrOrNullValue($room, 'preis')),
|
||||
'available' => $this->getIntAttrOrNullValue($room, 'verfuegbar'),
|
||||
'status' => $this->getAttrOrNullValue($room, 'status'),
|
||||
'boardCode' => $this->getAttrOrNullValue($room, 'vpcode'),
|
||||
'boardBusProId' => $this->getIntAttrOrNullValue($room, 'idbuspro_vp'),
|
||||
];
|
||||
});
|
||||
|
||||
return $rooms;
|
||||
}
|
||||
|
||||
/**
|
||||
* The struck-through comparison price, carried by the PDGS pseudo room.
|
||||
*/
|
||||
public function getPseudoPrice(Crawler $hotelNode): ?float
|
||||
{
|
||||
foreach ($this->children($hotelNode, 'zimmer/preis') as $room) {
|
||||
$room = new Crawler($room);
|
||||
|
||||
if (Constants::PSEUDO_PRICE_ROOM_CODE === $this->trimOrNull($this->getAttrOrNullValue($room, 'zimmercode'))) {
|
||||
return $this->stringToFloat($this->getAttrOrNullValue($room, 'preis'));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The guide of the first outbound bus that has one.
|
||||
*
|
||||
* Not TravelParser::getGuide(), which takes the first guide of any bus, return leg
|
||||
* included.
|
||||
*
|
||||
* @return array{name: ?string, phone: ?string}|null
|
||||
*/
|
||||
public function getGuide(Crawler $dateNode): ?array
|
||||
{
|
||||
$guides = $this->children($dateNode, 'lei_befoerderung/leistung[@unterart="BUS"][not(richtung="RUECK")]/zustiegsplanung/reiseleiter');
|
||||
|
||||
if (0 === $guides->count()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$guide = $guides->first();
|
||||
|
||||
return [
|
||||
'name' => $this->getStringOrNullValue($this->children($guide, 'name')),
|
||||
'phone' => $this->getStringOrNullValue($this->children($guide, 'telefon')),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A path below the node's own children, never deeper.
|
||||
*
|
||||
* DomCrawler evaluates filterXPath() against a fake parent of the node, so `./x` finds
|
||||
* nothing and `.//x` searches every descendant -- which would pull a hotel's
|
||||
* `lei_sonstiges` into the date's list. A leading `*` step anchors the path at the
|
||||
* node itself, whatever its name.
|
||||
*/
|
||||
public function children(Crawler $node, string $path): Crawler
|
||||
{
|
||||
return $node->filterXPath('*/'.$path);
|
||||
}
|
||||
|
||||
private function trimOrNull(?string $value): ?string
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$trimmed = trim($value);
|
||||
|
||||
return '' === $trimmed ? null : $trimmed;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user