}> */ 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 */ 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> */ 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; } }