*/ public function loadAll(?string $filename = 'zustiege.xml'): array { try { return $this->cache->get('bpn_pickups', function (ItemInterface $item) use ($filename) { $item->expiresAfter(3 * 60 * 60); $item->tag(['xml-sync']); $crawler = $this->loadXml($filename); $pickupNodes = $crawler->filterXPath('//zustiege/zustieg'); $pickups = []; $pickupNodes->each(function (Crawler $node) use (&$pickups) { $id = $node->attr('idbuspro'); $pickups[$id] = $this->parseXml($node); }); return $pickups; }); } catch (InvalidArgumentException $e) { return []; } } public function loadById(int $id, ?string $filename = 'zustiege.xml'): ?Pickup { $pickups = $this->loadAll($filename); return $pickups[$id] ?? null; } private function loadXml(?string $filename = 'zustiege.xml'): Crawler { $xml = $this->xmlExport->read($filename); return new Crawler($xml); } public function parseXml(Crawler $node): Pickup { $pickup = new Pickup(); $pickup->id = (int) $node->attr('idbuspro'); $pickup->code = $node->attr('code'); $pickup->city = $this->getStringOrNullValue($node->filterXPath('//ort')); $pickup->postalCode = $this->getStringOrNullValue($node->filterXPath('//plz')); $pickup->street = $this->getStringOrNullValue($node->filterXPath('//strasse')); return $pickup; } public function patchPickupsDetails(Travel $travel): void { $travel->pickups = $this->patchAndSortPickups($travel->pickups); $travel->dropOffs = $this->patchAndOrderDropOffs($travel->dropOffs, $travel->pickups); } /** * @param array $pickups * * @return array */ private function patchAndSortPickups(array $pickups): array { $this->enrichPickupDetails($pickups); uasort($pickups, fn (Pickup $a, Pickup $b): int => $this->comparePickupsByTime($a, $b)); return $pickups; } /** * Enriches drop-off details and determines their order. * * When drop-offs carry their own time data, they are sorted chronologically. * Otherwise the return journey order is derived by reversing the chronologically * sorted outbound pickup order. Any drop-offs without a matching pickup ID are * appended at the end. * * @param array $dropOffs Drop-offs indexed by ID * @param array $sortedPickups Already sorted pickups indexed by ID * * @return array Ordered drop-offs */ private function patchAndOrderDropOffs(array $dropOffs, array $sortedPickups): array { $this->enrichPickupDetails($dropOffs); if ($this->hasTimeData($dropOffs)) { uasort($dropOffs, fn (Pickup $a, Pickup $b): int => $this->comparePickupsByTime($a, $b)); return $dropOffs; } // Fallback: order drop-offs in reverse of the sorted pickup route $reversedPickupIds = array_reverse(array_keys($sortedPickups)); $ordered = []; foreach ($reversedPickupIds as $id) { if (isset($dropOffs[$id])) { $ordered[$id] = $dropOffs[$id]; } } // Append any drop-offs not present in pickups foreach ($dropOffs as $id => $dropOff) { if (false === isset($ordered[$id])) { $ordered[$id] = $dropOff; } } return $ordered; } /** * @param array $pickups */ private function hasTimeData(array $pickups): bool { foreach ($pickups as $pickup) { if (null !== $pickup->time) { return true; } } return false; } /** * @param array $pickups Pickups indexed by ID (modified in place) */ private function enrichPickupDetails(array $pickups): void { foreach ($pickups as $pickup) { $pickupData = $this->loadById($pickup->id); if (null === $pickupData) { continue; } $pickup->code = $pickupData->code; $pickup->postalCode = $pickupData->postalCode; $pickup->city = $pickupData->city; $pickup->street = $pickupData->street; } } private function comparePickupsByTime(Pickup $a, Pickup $b): int { if (null === $a->time && null === $b->time) { return 0; } if (null === $a->time) { return -1; } if (null === $b->time) { return 1; } return $a->time <=> $b->time; } }