>|null */ private ?array $filesMapCache = null; public function __construct( private readonly TravelLoader $travelLoader, private readonly HotelLoader $hotelLoader, private readonly TravelSnapshotManager $travelSnapshotService, private readonly LoggerInterface $logger, ) { } /** * Generate the combined files mapping from XML and snapshot sources. * * XML entries take precedence; snapshot entries fill in any gaps. The result is * memoized per request so multiple callers within the same request pay no extra cost. * * @return array> */ public function generateFilesMap(): array { if (null !== $this->filesMapCache) { return $this->filesMapCache; } $mapping = []; try { $mapping = $this->travelLoader->generateFilesMap(); } catch (\Exception $e) { $this->logger->warning('Failed to generate XML files mapping, fallback to snapshots only', [ 'error' => $e->getMessage(), ]); } $snapshotMapping = $this->travelSnapshotService->generateMapping(); foreach ($snapshotMapping as $dateId => $snapshotEntry) { if (false === isset($mapping[$dateId])) { $mapping[$dateId] = $snapshotEntry; continue; } foreach ($snapshotEntry['hotels'] as $hotelId => $hotelData) { if (false === isset($mapping[$dateId]['hotels'][$hotelId])) { $mapping[$dateId]['hotels'][$hotelId] = $hotelData; } } } $this->logger->debug('Generated files mapping', [ 'count' => count($mapping), ]); $this->filesMapCache = $mapping; return $mapping; } /** * Convert a date code string to its corresponding date ID. * * @param string $dateCode The date code to resolve * * @return int|null The date ID, or null if the code is not found */ public function mapDateCodeToId(string $dateCode): ?int { try { $mapping = $this->generateFilesMap(); $dateCodes = array_column($mapping, 'code', 'id'); $dateId = array_search($dateCode, $dateCodes, true); if (false === $dateId) { return null; } $this->logger->debug('Date code mapping', [ 'dateCode' => $dateCode, 'dateId' => $dateId, ]); return (int) $dateId; } catch (\Exception $e) { $this->logger->error('Failed to map date code to ID', [ 'dateCode' => $dateCode, 'error' => $e->getMessage(), ]); return null; } } /** * Convert a hotel code string to its corresponding hotel ID. * * @param string $hotelCode The hotel code to resolve * * @return int|null The hotel ID, or null if the code is not found */ public function mapHotelCodeToId(string $hotelCode): ?int { try { $hotelId = $this->hotelLoader->mapCodeToId($hotelCode); $this->logger->debug('Hotel code mapping', [ 'hotelCode' => $hotelCode, 'hotelId' => $hotelId, ]); return $hotelId; } catch (\Exception $e) { $this->logger->error('Failed to map hotel code to ID', [ 'hotelCode' => $hotelCode, 'error' => $e->getMessage(), ]); return null; } } /** * Convert a date ID to its corresponding product ID for API calls. * * Prefers the product ID stored in the snapshot (fast DB lookup) before * falling back to the XML loader's filename-based resolution. * * @param int $dateId The date ID to resolve * * @return int|null The product ID, or null if it cannot be determined */ public function mapDateIdToProductId(int $dateId): ?int { try { $productId = $this->travelSnapshotService->findProductIdByDateId($dateId); $productId = $productId ?? $this->travelLoader->mapDateIdToProductId($dateId); $this->logger->debug('Date ID to product ID mapping', [ 'dateId' => $dateId, 'productId' => $productId, ]); return $productId; } catch (\Exception $e) { $this->logger->error('Failed to map date ID to product ID', [ 'dateId' => $dateId, 'error' => $e->getMessage(), ]); return null; } } /** * Check whether travel data exists in any local persisted source. * * Performs a lightweight existence check: the snapshot DB is consulted first * and the XML files map is only scanned if no snapshot is found. * * @param int $dateId The travel date ID to check * @param int|null $hotelId Optional hotel ID * * @return bool True if data exists locally */ public function existsLocally(int $dateId, ?int $hotelId = null): bool { if (true === $this->travelSnapshotService->exists($dateId, $hotelId)) { return true; } $mapping = $this->generateFilesMap(); if (false === isset($mapping[$dateId])) { return false; } if (null !== $hotelId && false === isset($mapping[$dateId]['hotels'][$hotelId])) { return false; } return true; } /** * Return availability flags for both local and remote sources. * * @param int $dateId The travel date ID to check * @param int|null $hotelId Optional hotel ID * * @return array{local: bool, remote: bool} */ public function getAvailableSources(int $dateId, ?int $hotelId = null): array { return [ 'local' => $this->existsLocally($dateId, $hotelId), 'remote' => null !== $this->mapDateIdToProductId($dateId), ]; } }