feat: read the BusPro XML export through XmlProductReader
This commit is contained in:
1 parent
b9851943d0
commit
1b6f480bee
4 files changed
+479
-216
No files matched your search
@@ -29,9 +29,12 @@ namespace EP\EpProducts\Command;
|
||||
|
||||
use EP\EpProducts\Service\DateImportService;
|
||||
use EP\EpProducts\Service\ProductImportSourceFactory;
|
||||
use EP\EpProducts\Service\XmlProductReader;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use TYPO3\CMS\Core\Log\LogManager;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Service\CacheService;
|
||||
|
||||
@@ -94,7 +97,7 @@ class DateCommandController extends Command
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = $this->dateImportService->import($path);
|
||||
$count = $this->dateImportService->import(new XmlProductReader($path, GeneralUtility::makeInstance(LogManager::class)->getLogger(XmlProductReader::class)));
|
||||
$importSource->markImported($path);
|
||||
$output->writeln(sprintf('%d rows imported.', $count ));
|
||||
$settings = $this->getSettings();
|
||||
|
||||
@@ -85,11 +85,6 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
*/
|
||||
protected $hotelMappings = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $pickups = [];
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
*/
|
||||
@@ -146,18 +141,18 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param iterable<array> $rows one row per travel date × hotel, in the shape
|
||||
* of GET /api/exports/products on MyE&P
|
||||
* @return int
|
||||
* @throws \Doctrine\DBAL\DBALException
|
||||
*/
|
||||
public function import($path): int
|
||||
public function import(iterable $rows): int
|
||||
{
|
||||
$this->db = $this->getDbConnection();
|
||||
$this->getHotelMappings();
|
||||
$this->importPickups($path);
|
||||
$this->createTempTables();
|
||||
$this->logger->info('starting product import');
|
||||
$dateCount = $this->parseXmlFilesForProducts($path);
|
||||
$dateCount = $this->importProducts($rows);
|
||||
$this->clearTables();
|
||||
$this->copyTables();
|
||||
$this->clearTempTables();
|
||||
@@ -171,46 +166,33 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
return $this->importSourceFactory->get()->isNewImportAvailable();
|
||||
}
|
||||
|
||||
public function parseXmlFilesForProducts($path): int
|
||||
/**
|
||||
* Groups the rows by product and travel date, in the order they first
|
||||
* appear, because a product code can map to more than one TYPO3 product and
|
||||
* each of them imports all of the product's dates.
|
||||
*
|
||||
* @param iterable<array> $rows
|
||||
* @return int
|
||||
*/
|
||||
protected function importProducts(iterable $rows): int
|
||||
{
|
||||
$dateCount = 0;
|
||||
|
||||
$folder = opendir($path);
|
||||
while (($fileName = readdir($folder)) !== false) {
|
||||
if (strpos($fileName, 'Ziel') === 0) {
|
||||
$filePath = $path . '/' . $fileName;
|
||||
$dateCount += $this->parseProductXmlFile($filePath);
|
||||
$products = [];
|
||||
foreach ($rows as $row) {
|
||||
$code = $row['product']['code'];
|
||||
if (!isset($products[$code])) {
|
||||
$products[$code] = ['busProId' => (int) $row['product']['busProId'], 'dates' => []];
|
||||
}
|
||||
$products[$code]['dates'][(int) $row['date']['busProId']][] = $row;
|
||||
}
|
||||
|
||||
$dateCount = 0;
|
||||
foreach ($products as $code => $product) {
|
||||
$dateCount += $this->importProduct((string) $code, $product['busProId'], $product['dates']);
|
||||
}
|
||||
|
||||
return $dateCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $file
|
||||
* @return int
|
||||
*/
|
||||
public function parseProductXmlFile($file): int
|
||||
{
|
||||
if (!file_exists($file)) {
|
||||
$this->logger->error('product xml not found', ['file' => $file]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
/** @var \SimpleXMLElement $xmlData */
|
||||
if (!$xmlData = simplexml_load_string(file_get_contents($file))) {
|
||||
libxml_clear_errors();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($xmlData->reise[0] === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->parseProduct($xmlData->reise[0]);
|
||||
}
|
||||
|
||||
public function createTempTables(): void
|
||||
{
|
||||
$this->db->executeStatement('CREATE TABLE IF NOT EXISTS tx_epproducts_domain_model_date_temp LIKE tx_epproducts_domain_model_date');
|
||||
@@ -236,13 +218,13 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \SimpleXMLElement $xmlProduct
|
||||
* @param string $code
|
||||
* @param int $busProId
|
||||
* @param array[][] $dates rows grouped by travel date
|
||||
* @return int
|
||||
*/
|
||||
protected function parseProduct(\SimpleXMLElement $xmlProduct): int
|
||||
protected function importProduct(string $code, int $busProId, array $dates): int
|
||||
{
|
||||
$code = (string) $xmlProduct->attributes()['code'];
|
||||
$busProId = (int) $xmlProduct->attributes()['idbuspro'];
|
||||
$dateCount = 0;
|
||||
|
||||
$sql = '
|
||||
@@ -280,8 +262,8 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
$product['bus_pro_id'] = $busProId;
|
||||
$product['product_fact'] = $this->getTopFactForRecord('tx_epproducts_product_fact_mm', $product['uid']);
|
||||
$product['region_fact'] = $this->getTopFactForRecord('tx_epproducts_region_fact_mm', $product['region']);
|
||||
foreach ($xmlProduct->termin as $xmlDate) {
|
||||
$dateCount += $this->parseDates($xmlDate, $product);
|
||||
foreach ($dates as $rows) {
|
||||
$dateCount += $this->importDate($rows, $product);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,25 +271,18 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \SimpleXMLElement $xmlDate
|
||||
* @param array[] $rows one per hotel of the travel date
|
||||
* @param array $product
|
||||
* @return int
|
||||
*/
|
||||
protected function parseDates(\SimpleXMLElement $xmlDate, array $product): int
|
||||
protected function importDate(array $rows, array $product): int
|
||||
{
|
||||
$dateCount = 0;
|
||||
// $earliestDate = new \DateTimeImmutable('next year');
|
||||
|
||||
foreach ($xmlDate->hotel as $xmlHotel) {
|
||||
// don't import dates starting later than one year from now
|
||||
// $dateStart = \DateTime::createFromFormat('d.m.Y', (string) $xmlDate->attributes()['termin']);
|
||||
// if ($dateStart > $earliestDate) {
|
||||
// continue;
|
||||
// }
|
||||
foreach ($rows as $row) {
|
||||
$date = $this->createDateRecord($product, $row);
|
||||
|
||||
$date = $this->createDateRecord($product, $xmlDate);
|
||||
|
||||
$hotelCode = (string) $xmlHotel->attributes()['code'];
|
||||
$hotelCode = $row['hotel']['code'];
|
||||
$key = $hotelCode . $date['date_start'];
|
||||
|
||||
// Import daytrip dates only once per hotel as room information is taken
|
||||
@@ -316,7 +291,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->parseHotel($xmlHotel, $date) !== false) {
|
||||
if ($this->parseHotel($row, $date) !== false) {
|
||||
$this->db->insert(
|
||||
'tx_epproducts_domain_model_date_temp',
|
||||
$date
|
||||
@@ -342,17 +317,17 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
|
||||
/**
|
||||
* @param array $product
|
||||
* @param \SimpleXMLElement $xmlDate
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
protected function createDateRecord(array $product, \SimpleXMLElement $xmlDate): array
|
||||
protected function createDateRecord(array $product, array $row): array
|
||||
{
|
||||
$dateStart = \DateTime::createFromFormat('d.m.Y', (string) $xmlDate->attributes()['termin']);
|
||||
$dateEnd = \DateTime::createFromFormat('d.m.Y', (string) $xmlDate->attributes()['bis']);
|
||||
$dateStart = \DateTime::createFromFormat('Y-m-d', $row['date']['from']);
|
||||
$dateEnd = \DateTime::createFromFormat('Y-m-d', $row['date']['to']);
|
||||
$nights = $dateStart->diff($dateEnd)->format('%d');
|
||||
|
||||
$busProId = (int) $xmlDate->attributes()['idbuspro'];
|
||||
$code = (string) $xmlDate->attributes()['code'];
|
||||
$busProId = (int) $row['date']['busProId'];
|
||||
$code = (string) $row['date']['code'];
|
||||
|
||||
$date = [
|
||||
'pid' => $this->pid,
|
||||
@@ -395,15 +370,15 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
'important' => $product['important'],
|
||||
];
|
||||
|
||||
$includedAccommodation = $this->parseSelectionGroup($xmlDate, static::SELECTION_GROUP_ACCOMMODATION);
|
||||
$includedSkiPass = $this->parseSelectionGroup($xmlDate, static::SELECTION_GROUP_SKIPASS);
|
||||
$includedBoard = $this->parseSelectionGroup($xmlDate, static::SELECTION_GROUP_BOARD);
|
||||
$includedTransportation = $this->parseSelectionGroup($xmlDate, static::SELECTION_GROUP_TRANSPORTATION);
|
||||
$includedProgramme = $this->parseSelectionGroup($xmlDate, static::SELECTION_GROUP_PROGRAMME);
|
||||
$includedPersonnel = $this->parsePersonnelSelectionGroup($xmlDate);
|
||||
$includedEvents = $this->parseSelectionGroup($xmlDate, static::SELECTION_GROUP_EVENTS);
|
||||
$includedOther = $this->parseSelectionGroup($xmlDate, static::SELECTION_GROUP_OTHER);
|
||||
$includedCourses = $this->parseSelectionGroup($xmlDate, static::SELECTION_GROUP_COURSES);
|
||||
$includedAccommodation = $this->parseSelectionGroup($row['selectionGroups'], static::SELECTION_GROUP_ACCOMMODATION);
|
||||
$includedSkiPass = $this->parseSelectionGroup($row['selectionGroups'], static::SELECTION_GROUP_SKIPASS);
|
||||
$includedBoard = $this->parseSelectionGroup($row['selectionGroups'], static::SELECTION_GROUP_BOARD);
|
||||
$includedTransportation = $this->parseSelectionGroup($row['selectionGroups'], static::SELECTION_GROUP_TRANSPORTATION);
|
||||
$includedProgramme = $this->parseSelectionGroup($row['selectionGroups'], static::SELECTION_GROUP_PROGRAMME);
|
||||
$includedPersonnel = $this->parsePersonnelSelectionGroup($row['selectionGroups']);
|
||||
$includedEvents = $this->parseSelectionGroup($row['selectionGroups'], static::SELECTION_GROUP_EVENTS);
|
||||
$includedOther = $this->parseSelectionGroup($row['selectionGroups'], static::SELECTION_GROUP_OTHER);
|
||||
$includedCourses = $this->parseSelectionGroup($row['selectionGroups'], static::SELECTION_GROUP_COURSES);
|
||||
|
||||
$includedServices = [
|
||||
...$includedAccommodation,
|
||||
@@ -428,7 +403,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
$skiPassIncluded = (int) (count($includedSkiPass) > 0);
|
||||
$date['skipass_included'] = $skiPassIncluded;
|
||||
|
||||
$servicesTransportation = $this->parseService($xmlDate, 'lei_befoerderung');
|
||||
$servicesTransportation = $this->parseService($row['transportServices']);
|
||||
$discount = $this->determineDiscount($servicesTransportation);
|
||||
$date['discount'] = $discount;
|
||||
|
||||
@@ -454,7 +429,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
];
|
||||
}
|
||||
|
||||
$servicesGeneral = $this->parseService($xmlDate, 'lei_sonstiges');
|
||||
$servicesGeneral = $this->parseService($row['dateServices']);
|
||||
|
||||
$combinedServices = [];
|
||||
|
||||
@@ -476,29 +451,25 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
$date['services_general'] = serialize($combinedServices);
|
||||
|
||||
$pickups = [];
|
||||
foreach ($xmlDate->xpath('zustiege/zustieg') as $pickup) {
|
||||
$attributes = $pickup->attributes();
|
||||
if (null === $attributes) {
|
||||
continue;
|
||||
}
|
||||
$busProId = (int) $attributes['idbuspro'];
|
||||
$timezone = new \DateTimeZone('Europe/Berlin');
|
||||
foreach ($row['pickups'] as $pickup) {
|
||||
$busProId = (int) $pickup['busProId'];
|
||||
if ($busProId === 0) {
|
||||
continue;
|
||||
}
|
||||
$timestamp = (string) $attributes['zeit'];
|
||||
$parts = explode(' ', $timestamp);
|
||||
if (2 === count($parts)) {
|
||||
[$pickupDate, $timePart] = $parts;
|
||||
$pickupTime = substr($timePart, 0, 5);
|
||||
} else {
|
||||
$pickupDate = $dateStart->format('d.m.Y');
|
||||
$pickupTime = $parts[0];
|
||||
$pickupDate = $dateStart->format('d.m.Y');
|
||||
$pickupTime = '';
|
||||
if ($pickup['datetime'] !== null) {
|
||||
$pickupDateTime = (new \DateTimeImmutable($pickup['datetime']))->setTimezone($timezone);
|
||||
$pickupDate = $pickupDateTime->format('d.m.Y');
|
||||
$pickupTime = $pickupDateTime->format('H:i');
|
||||
}
|
||||
$pickups[] = [
|
||||
'bus_pro_id' => $busProId,
|
||||
'city' => $this->pickups[$busProId]['city'],
|
||||
'street' => $this->pickups[$busProId]['street'],
|
||||
'price' => (float) str_replace(',', '.', (string) $attributes['preis']),
|
||||
'city' => $pickup['city'],
|
||||
'street' => $pickup['street'],
|
||||
// A pickup without a stated price has always been shown as included.
|
||||
'price' => $pickup['price'] === null ? 0.0 : (float) $pickup['price'],
|
||||
'date' => $pickupDate,
|
||||
'time' => $pickupTime,
|
||||
];
|
||||
@@ -515,21 +486,20 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
});
|
||||
$date['pickups'] = serialize($pickups);
|
||||
|
||||
$guide = $this->parseGuide($xmlDate);
|
||||
$date['guide'] = serialize($guide);
|
||||
$date['guide'] = serialize($row['guide'] ?? []);
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \SimpleXMLElement $xmlHotel
|
||||
* @param array $row
|
||||
* @param array $date
|
||||
* @return bool
|
||||
*/
|
||||
protected function parseHotel(\SimpleXMLElement $xmlHotel, array &$date): bool
|
||||
protected function parseHotel(array $row, array &$date): bool
|
||||
{
|
||||
$code = (string) $xmlHotel->attributes()['code'];
|
||||
$busProId = (string) $xmlHotel->attributes()['idbuspro'];
|
||||
$code = $row['hotel']['code'];
|
||||
$busProId = (int) $row['hotel']['busProId'];
|
||||
|
||||
if (!array_key_exists($code, $this->hotelMappings)) {
|
||||
$this->logger->warning('hotel not found', ['code' => $code]);
|
||||
@@ -542,7 +512,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
|
||||
$services = [];
|
||||
$generalServices = unserialize($date['services_general']);
|
||||
$hotelServices = $this->parseService($xmlHotel, 'lei_sonstiges', true);
|
||||
$hotelServices = $this->parseService($row['hotelServices'], true);
|
||||
$this->addServices($generalServices, $services);
|
||||
$this->addServices($hotelServices, $services);
|
||||
foreach ($services as $key => $list) {
|
||||
@@ -552,7 +522,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
$services[$key] = $list;
|
||||
}
|
||||
|
||||
$this->parseRooms($xmlHotel, $hotel['uid'], $date, $services);
|
||||
$this->parseRooms($row, $hotel['uid'], $date, $services);
|
||||
|
||||
$date['hotel'] = $hotel['uid'];
|
||||
$date['hotel_code'] = $hotel['code'];
|
||||
@@ -571,34 +541,13 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function parseGuide(\SimpleXMLElement $xmlDate)
|
||||
{
|
||||
$transportationServicesBus = $xmlDate->xpath('lei_befoerderung/leistung[@unterart="BUS"]');
|
||||
|
||||
foreach ($transportationServicesBus as $service) {
|
||||
if ('RUECK' === (string) $service->richtung) {
|
||||
continue;
|
||||
}
|
||||
if (false === isset($service->zustiegsplanung->reiseleiter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => (string) $service->zustiegsplanung->reiseleiter->name,
|
||||
'phone' => (string) $service->zustiegsplanung->reiseleiter->telefon,
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \SimpleXMLElement $xmlHotel
|
||||
* @param array $row
|
||||
* @param int $hotelUid
|
||||
* @param array $date
|
||||
* @param array $services
|
||||
*/
|
||||
protected function parseRooms(\SimpleXMLElement $xmlHotel, $hotelUid, array &$date, array $services): void
|
||||
protected function parseRooms(array $row, $hotelUid, array &$date, array $services): void
|
||||
{
|
||||
$minPriceOverall = 0;
|
||||
$minPriceAvailable = 0;
|
||||
@@ -607,21 +556,18 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
$boardCode = '';
|
||||
$count = 0;
|
||||
|
||||
foreach ($xmlHotel->xpath('zimmer/preis') as $roomXml) {
|
||||
$code = trim((string) $roomXml->attributes()['zimmercode']);
|
||||
if ($row['pseudoPrice'] !== null) {
|
||||
$date['pseudo_price'] = (int) $row['pseudoPrice'];
|
||||
}
|
||||
|
||||
if ($code === static::CODE_PSEUDOPRICE) {
|
||||
$price = (int) $roomXml->attributes()['preis'];
|
||||
$date['pseudo_price'] = $price;
|
||||
continue;
|
||||
}
|
||||
foreach ($row['rooms'] as $roomRow) {
|
||||
$code = $roomRow['code'];
|
||||
$label = $roomRow['label'];
|
||||
$roomMinPax = (int) $roomRow['minPax'];
|
||||
$pax = (int) $roomRow['maxPax'];
|
||||
|
||||
$label = (string) $roomXml->attributes()['zimmertext'];
|
||||
$roomMinPax = (int) $roomXml->attributes()['MinPax'];
|
||||
$pax = (int) $roomXml->attributes()['MaxPax'];
|
||||
|
||||
$available = (int) $roomXml->attributes()['verfuegbar'];
|
||||
$price = (int) $roomXml->attributes()['preis'];
|
||||
$available = (int) $roomRow['available'];
|
||||
$price = (int) $roomRow['price'];
|
||||
|
||||
// Skip rooms with zero price _and_ zero availability.
|
||||
if (!$available && !$price) {
|
||||
@@ -658,28 +604,28 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
}
|
||||
|
||||
// Determine status
|
||||
$statusXml = (string) $roomXml->attributes()['status'];
|
||||
$sourceStatus = $roomRow['status'];
|
||||
|
||||
if (static::STATUS_STOP === $statusXml && $price) {
|
||||
if (static::STATUS_STOP === $sourceStatus && $price) {
|
||||
$status = Room::ROOM_STATUS_FULLY_BOOKED;
|
||||
} elseif (static::STATUS_ON_REQUEST === $statusXml) {
|
||||
} elseif (static::STATUS_ON_REQUEST === $sourceStatus) {
|
||||
$status = Room::ROOM_STATUS_ON_REQUEST;
|
||||
} else {
|
||||
$status = Room::ROOM_STATUS_AVAILABLE;
|
||||
}
|
||||
|
||||
$boardCode = (string) $roomXml->attributes()['vpcode'];
|
||||
$boardCode = $roomRow['boardCode'];
|
||||
|
||||
$room = [
|
||||
'pid' => $this->pid,
|
||||
'date' => 0,
|
||||
'hotel' => $hotelUid,
|
||||
'bus_pro_id' => (int) $roomXml->attributes()['idbuspro_zimmer'],
|
||||
'bus_pro_id' => (int) $roomRow['busProId'],
|
||||
'code' => $code,
|
||||
'type' => $roomType,
|
||||
'name' => $label,
|
||||
'pax' => $pax,
|
||||
'nights' => (int) $roomXml->attributes()['naechte'],
|
||||
'nights' => (int) $roomRow['nights'],
|
||||
'price' => $price,
|
||||
'available' => $available * $pax,
|
||||
'status' => $status,
|
||||
@@ -719,29 +665,26 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \SimpleXMLElement $xmlData
|
||||
* @param string $serviceLabel
|
||||
* @param array[] $services a service list of a row
|
||||
* @param bool $skipWithoutPrice
|
||||
* @return array
|
||||
*/
|
||||
protected function parseService(\SimpleXMLElement $xmlData, string $serviceLabel, bool $skipWithoutPrice = false): array
|
||||
protected function parseService(array $services, bool $skipWithoutPrice = false): array
|
||||
{
|
||||
$data = [];
|
||||
$services = $xmlData->xpath($serviceLabel . '/leistung');
|
||||
foreach ($services as $service) {
|
||||
// Skip transportation service direction 'back'
|
||||
if ($service->richtung && (string) $service->richtung === 'RUECK') {
|
||||
if ($service['direction'] === 'RUECK') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip services with status 'Buchungsstop'
|
||||
$status = (string) $service->status;
|
||||
if ($status === 'Buchungsstop') {
|
||||
if ($service['status'] === static::STATUS_STOP) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip services of subcategory 'EIN'
|
||||
$type = (string) $service->attributes()['unterart'];
|
||||
$type = $service['subType'];
|
||||
if ($type === 'EIN') {
|
||||
continue;
|
||||
}
|
||||
@@ -751,13 +694,14 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
$type = 'VER';
|
||||
}
|
||||
|
||||
$price = (float) str_replace(',', '.', $service->preis);
|
||||
// A service without a stated price has always been shown as included.
|
||||
$price = $service['price'] === null ? 0.0 : (float) $service['price'];
|
||||
|
||||
if ($skipWithoutPrice && $price === 0.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = (string) $service->text;
|
||||
$label = $service['label'];
|
||||
if ($type === 'BUS') {
|
||||
$label = static::LABEL_BUS;
|
||||
} elseif ($type === 'BAH') {
|
||||
@@ -768,8 +712,8 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
$data[$type] = [];
|
||||
}
|
||||
|
||||
$data[$type][(int)$service->attributes()['idbuspro']] = [
|
||||
'idBusPro' => (int) $service->attributes()['idbuspro'],
|
||||
$data[$type][(int) $service['busProId']] = [
|
||||
'idBusPro' => (int) $service['busProId'],
|
||||
'label' => $label,
|
||||
'price' => number_format($price, 2, ',', '.'),
|
||||
];
|
||||
@@ -781,46 +725,17 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \SimpleXMLElement $xmlDate
|
||||
* @param array[] $selectionGroups
|
||||
* @param int $busProId
|
||||
* @return array
|
||||
*/
|
||||
protected function parseSelectionGroup(\SimpleXMLElement $xmlDate, int $busProId): array
|
||||
{
|
||||
return $this->parseSelections($xmlDate->xpath('selektiongruppe[@idbuspro="' . $busProId . '"]/selektion'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \SimpleXMLElement $xmlDate
|
||||
* @return array
|
||||
*/
|
||||
protected function parsePersonnelSelectionGroup(\SimpleXMLElement $xmlDate): array
|
||||
{
|
||||
$groups = $xmlDate->xpath('selektiongruppe[@bezeichnung="' . static::SELECTION_GROUP_LABEL_PERSONNEL . '"]');
|
||||
foreach ($groups as $group) {
|
||||
$this->logger->warning(
|
||||
'selection group matched by label, register its id',
|
||||
['label' => static::SELECTION_GROUP_LABEL_PERSONNEL, 'idbuspro' => (int) $group->attributes()['idbuspro']]
|
||||
);
|
||||
}
|
||||
|
||||
return $this->parseSelections($xmlDate->xpath('selektiongruppe[@bezeichnung="' . static::SELECTION_GROUP_LABEL_PERSONNEL . '"]/selektion'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \SimpleXMLElement[] $selections
|
||||
* @return array
|
||||
*/
|
||||
protected function parseSelections(array $selections): array
|
||||
protected function parseSelectionGroup(array $selectionGroups, int $busProId): array
|
||||
{
|
||||
$data = [];
|
||||
foreach ($selections as $selection) {
|
||||
$attributes = $selection->attributes();
|
||||
if (null === $attributes) {
|
||||
continue;
|
||||
foreach ($selectionGroups as $group) {
|
||||
if ((int) $group['busProId'] === $busProId) {
|
||||
$this->addSelections($group['selections'], $data);
|
||||
}
|
||||
$id = (int) $attributes['idbuspro'];
|
||||
$data[$id] = (string) $attributes['bezeichnung'];
|
||||
}
|
||||
|
||||
ksort($data);
|
||||
@@ -828,6 +743,40 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array[] $selectionGroups
|
||||
* @return array
|
||||
*/
|
||||
protected function parsePersonnelSelectionGroup(array $selectionGroups): array
|
||||
{
|
||||
$data = [];
|
||||
foreach ($selectionGroups as $group) {
|
||||
if ($group['label'] !== static::SELECTION_GROUP_LABEL_PERSONNEL) {
|
||||
continue;
|
||||
}
|
||||
$this->logger->warning(
|
||||
'selection group matched by label, register its id',
|
||||
['label' => $group['label'], 'idbuspro' => (int) $group['busProId']]
|
||||
);
|
||||
$this->addSelections($group['selections'], $data);
|
||||
}
|
||||
|
||||
ksort($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array[] $selections
|
||||
* @param array $data
|
||||
*/
|
||||
protected function addSelections(array $selections, array &$data): void
|
||||
{
|
||||
foreach ($selections as $selection) {
|
||||
$data[(int) $selection['busProId']] = $selection['label'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $services
|
||||
* @param array $date
|
||||
@@ -915,27 +864,4 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
|
||||
}
|
||||
}
|
||||
|
||||
protected function importPickups(string $path): void
|
||||
{
|
||||
libxml_use_internal_errors (true);
|
||||
$file = $path . '/zustiege.xml';
|
||||
if (!is_file($file) || !$xmlData = simplexml_load_string((string) file_get_contents($file))) {
|
||||
libxml_clear_errors();
|
||||
$this->logger->warning('pickup xml missing or invalid', ['file' => $file]);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($xmlData->zustieg as $entry) {
|
||||
$attributes = $entry->attributes();
|
||||
if (null === $attributes) {
|
||||
continue;
|
||||
}
|
||||
$idBusPro = (int) $attributes['idbuspro'];
|
||||
$this->pickups[$idBusPro] = [
|
||||
'city' => (string) $entry->ort,
|
||||
'street' => (string) $entry->strasse,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
|
||||
namespace EP\EpProducts\Service;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* Reads a BusPro XML export directory (Ziel_*.xml, zustiege.xml) and yields one
|
||||
* row per travel date × hotel, in exactly the shape of the rows served by
|
||||
* GET /api/exports/products on MyE&P, so {@see DateImportService::import()}
|
||||
* does not care which of the two it is fed.
|
||||
*
|
||||
* Nothing is filtered or rewritten here: every display rule stays in
|
||||
* DateImportService. Prices are parsed from the German format, and a price the
|
||||
* source does not state is null.
|
||||
*/
|
||||
final class XmlProductReader implements \IteratorAggregate
|
||||
{
|
||||
private const PRODUCT_PREFIX = 'Ziel';
|
||||
private const PICKUPS_FILE = 'zustiege.xml';
|
||||
private const CODE_PSEUDOPRICE = 'PDGS';
|
||||
private const TIMEZONE = 'Europe/Berlin';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $path;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* @var \DateTimeZone
|
||||
*/
|
||||
private $timezone;
|
||||
|
||||
/**
|
||||
* @var array|null pickup master data by BusPro id
|
||||
*/
|
||||
private $pickups;
|
||||
|
||||
public function __construct(string $path, ?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->path = rtrim($path, '/');
|
||||
$this->logger = $logger ?? new NullLogger();
|
||||
$this->timezone = new \DateTimeZone(self::TIMEZONE);
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<array>
|
||||
*/
|
||||
public function getIterator(): \Generator
|
||||
{
|
||||
if ($this->pickups === null) {
|
||||
$this->pickups = $this->readPickups();
|
||||
}
|
||||
|
||||
$folder = opendir($this->path);
|
||||
if ($folder === false) {
|
||||
$this->logger->error('product xml directory not readable', ['path' => $this->path]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
while (($fileName = readdir($folder)) !== false) {
|
||||
if (strpos($fileName, self::PRODUCT_PREFIX) === 0) {
|
||||
yield from $this->readProductFile($this->path . '/' . $fileName);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
closedir($folder);
|
||||
}
|
||||
}
|
||||
|
||||
private function readProductFile(string $file): \Generator
|
||||
{
|
||||
libxml_use_internal_errors(true);
|
||||
$xmlData = simplexml_load_string((string) file_get_contents($file));
|
||||
if ($xmlData === false) {
|
||||
libxml_clear_errors();
|
||||
$this->logger->error('product xml invalid', ['file' => $file]);
|
||||
return;
|
||||
}
|
||||
|
||||
$xmlProduct = $xmlData->reise[0];
|
||||
if ($xmlProduct === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$product = [
|
||||
'code' => (string) $xmlProduct['code'],
|
||||
'busProId' => (int) $xmlProduct['idbuspro'],
|
||||
];
|
||||
|
||||
foreach ($xmlProduct->termin as $xmlDate) {
|
||||
$from = $this->parseDate((string) $xmlDate['termin']);
|
||||
$to = $this->parseDate((string) $xmlDate['bis']);
|
||||
if ($from === null || $to === null) {
|
||||
$this->logger->warning('travel date skipped, dates unparseable', [
|
||||
'code' => (string) $xmlDate['code'],
|
||||
'termin' => (string) $xmlDate['termin'],
|
||||
'bis' => (string) $xmlDate['bis'],
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$date = [
|
||||
'code' => (string) $xmlDate['code'],
|
||||
'busProId' => (int) $xmlDate['idbuspro'],
|
||||
'from' => $from->format('Y-m-d'),
|
||||
'to' => $to->format('Y-m-d'),
|
||||
'type' => (string) $xmlDate['reiseart'],
|
||||
];
|
||||
|
||||
// Everything below is per travel date and shared by its hotels.
|
||||
$selectionGroups = $this->readSelectionGroups($xmlDate);
|
||||
$transportServices = $this->readServices($xmlDate, 'lei_befoerderung');
|
||||
$dateServices = $this->readServices($xmlDate, 'lei_sonstiges');
|
||||
$pickups = $this->readDatePickups($xmlDate, $from);
|
||||
$guide = $this->readGuide($xmlDate);
|
||||
|
||||
foreach ($xmlDate->hotel as $xmlHotel) {
|
||||
[$pseudoPrice, $rooms] = $this->readRooms($xmlHotel);
|
||||
|
||||
yield [
|
||||
'product' => $product,
|
||||
'date' => $date,
|
||||
'hotel' => [
|
||||
'code' => (string) $xmlHotel['code'],
|
||||
'busProId' => (int) $xmlHotel['idbuspro'],
|
||||
],
|
||||
'pseudoPrice' => $pseudoPrice,
|
||||
'selectionGroups' => $selectionGroups,
|
||||
'transportServices' => $transportServices,
|
||||
'dateServices' => $dateServices,
|
||||
'hotelServices' => $this->readServices($xmlHotel, 'lei_sonstiges'),
|
||||
'rooms' => $rooms,
|
||||
'pickups' => $pickups,
|
||||
'guide' => $guide,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function readSelectionGroups(\SimpleXMLElement $xmlDate): array
|
||||
{
|
||||
$groups = [];
|
||||
foreach ($xmlDate->selektiongruppe as $xmlGroup) {
|
||||
$selections = [];
|
||||
foreach ($xmlGroup->selektion as $xmlSelection) {
|
||||
$selections[] = [
|
||||
'busProId' => (int) $xmlSelection['idbuspro'],
|
||||
'label' => (string) $xmlSelection['bezeichnung'],
|
||||
];
|
||||
}
|
||||
$groups[] = [
|
||||
'busProId' => (int) $xmlGroup['idbuspro'],
|
||||
'label' => (string) $xmlGroup['bezeichnung'],
|
||||
'selections' => $selections,
|
||||
];
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
private function readServices(\SimpleXMLElement $xmlParent, string $list): array
|
||||
{
|
||||
$services = [];
|
||||
foreach ($xmlParent->xpath($list . '/leistung') as $xmlService) {
|
||||
$direction = trim((string) $xmlService->richtung);
|
||||
$services[] = [
|
||||
'busProId' => (int) $xmlService['idbuspro'],
|
||||
'subType' => (string) $xmlService['unterart'],
|
||||
'label' => (string) $xmlService->text,
|
||||
'price' => $this->parsePrice((string) $xmlService->preis),
|
||||
'status' => (string) $xmlService->status,
|
||||
'direction' => $direction === '' ? null : $direction,
|
||||
];
|
||||
}
|
||||
|
||||
return $services;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: ?float, 1: array}
|
||||
*/
|
||||
private function readRooms(\SimpleXMLElement $xmlHotel): array
|
||||
{
|
||||
$pseudoPrice = null;
|
||||
$rooms = [];
|
||||
foreach ($xmlHotel->xpath('zimmer/preis') as $xmlRoom) {
|
||||
$code = trim((string) $xmlRoom['zimmercode']);
|
||||
if ($code === self::CODE_PSEUDOPRICE) {
|
||||
$pseudoPrice = $this->parsePrice((string) $xmlRoom['preis']);
|
||||
continue;
|
||||
}
|
||||
|
||||
$rooms[] = [
|
||||
'busProId' => (int) $xmlRoom['idbuspro_zimmer'],
|
||||
'code' => $code,
|
||||
'label' => (string) $xmlRoom['zimmertext'],
|
||||
'category' => (string) $xmlRoom['kat'],
|
||||
'minPax' => $this->parseInt((string) $xmlRoom['MinPax']),
|
||||
'maxPax' => $this->parseInt((string) $xmlRoom['MaxPax']),
|
||||
'nights' => $this->parseInt((string) $xmlRoom['naechte']),
|
||||
'price' => $this->parsePrice((string) $xmlRoom['preis']),
|
||||
'available' => $this->parseInt((string) $xmlRoom['verfuegbar']),
|
||||
'status' => (string) $xmlRoom['status'],
|
||||
'boardCode' => (string) $xmlRoom['vpcode'],
|
||||
'boardBusProId' => $this->parseInt((string) $xmlRoom['idbuspro_vp']),
|
||||
];
|
||||
}
|
||||
|
||||
return [$pseudoPrice, $rooms];
|
||||
}
|
||||
|
||||
/**
|
||||
* A bare HH:mm takes the travel date's start, and is rolled onto the next
|
||||
* day when it would otherwise fall before the latest full date-time listed
|
||||
* before it: a coach does not board backwards.
|
||||
*/
|
||||
private function readDatePickups(\SimpleXMLElement $xmlDate, \DateTimeImmutable $from): array
|
||||
{
|
||||
$pickups = [];
|
||||
$latest = null;
|
||||
foreach ($xmlDate->xpath('zustiege/zustieg') as $xmlPickup) {
|
||||
$busProId = (int) $xmlPickup['idbuspro'];
|
||||
$time = trim((string) $xmlPickup['zeit']);
|
||||
|
||||
$datetime = null;
|
||||
if ($time !== '' && strpos($time, ' ') !== false) {
|
||||
$datetime = \DateTimeImmutable::createFromFormat('!d.m.Y H:i:s', $time, $this->timezone) ?: null;
|
||||
if ($datetime !== null && ($latest === null || $datetime > $latest)) {
|
||||
$latest = $datetime;
|
||||
}
|
||||
} elseif ($time !== '') {
|
||||
$datetime = \DateTimeImmutable::createFromFormat('!Y-m-d H:i', $from->format('Y-m-d') . ' ' . $time, $this->timezone) ?: null;
|
||||
if ($datetime !== null && $latest !== null && $datetime < $latest) {
|
||||
$datetime = $datetime->modify('+1 day');
|
||||
}
|
||||
}
|
||||
|
||||
$masterData = $this->pickups[$busProId] ?? null;
|
||||
$pickups[] = [
|
||||
'busProId' => $busProId,
|
||||
'code' => $masterData['code'] ?? null,
|
||||
'city' => $masterData['city'] ?? null,
|
||||
'street' => $masterData['street'] ?? null,
|
||||
'price' => $this->parsePrice((string) $xmlPickup['preis']),
|
||||
'datetime' => $datetime !== null ? $datetime->format(\DateTimeInterface::ATOM) : null,
|
||||
];
|
||||
}
|
||||
|
||||
return $pickups;
|
||||
}
|
||||
|
||||
private function readGuide(\SimpleXMLElement $xmlDate): ?array
|
||||
{
|
||||
foreach ($xmlDate->xpath('lei_befoerderung/leistung[@unterart="BUS"]') as $xmlService) {
|
||||
if ((string) $xmlService->richtung === 'RUECK') {
|
||||
continue;
|
||||
}
|
||||
if (!isset($xmlService->zustiegsplanung->reiseleiter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => (string) $xmlService->zustiegsplanung->reiseleiter->name,
|
||||
'phone' => (string) $xmlService->zustiegsplanung->reiseleiter->telefon,
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{code: string, city: string, street: string}>
|
||||
*/
|
||||
private function readPickups(): array
|
||||
{
|
||||
$file = $this->path . '/' . self::PICKUPS_FILE;
|
||||
libxml_use_internal_errors(true);
|
||||
if (!is_file($file) || !$xmlData = simplexml_load_string((string) file_get_contents($file))) {
|
||||
libxml_clear_errors();
|
||||
$this->logger->warning('pickup xml missing or invalid', ['file' => $file]);
|
||||
return [];
|
||||
}
|
||||
|
||||
$pickups = [];
|
||||
foreach ($xmlData->zustieg as $xmlPickup) {
|
||||
$pickups[(int) $xmlPickup['idbuspro']] = [
|
||||
'code' => (string) $xmlPickup['code'],
|
||||
'city' => (string) $xmlPickup->ort,
|
||||
'street' => (string) $xmlPickup->strasse,
|
||||
];
|
||||
}
|
||||
|
||||
return $pickups;
|
||||
}
|
||||
|
||||
private function parseDate(string $value): ?\DateTimeImmutable
|
||||
{
|
||||
$date = \DateTimeImmutable::createFromFormat('!d.m.Y', trim($value), $this->timezone);
|
||||
|
||||
return $date ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* German format with an optional thousands separator: "1.234,50" is 1234.5.
|
||||
*/
|
||||
private function parsePrice(string $value): ?float
|
||||
{
|
||||
$value = str_replace(['.', ','], ['', '.'], trim($value));
|
||||
|
||||
return is_numeric($value) ? (float) $value : null;
|
||||
}
|
||||
|
||||
private function parseInt(string $value): ?int
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
return is_numeric($value) ? (int) $value : null;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ namespace EP\EpProducts\Task;
|
||||
|
||||
use EP\EpProducts\Service\DateImportService;
|
||||
use EP\EpProducts\Service\ProductImportSourceFactory;
|
||||
use EP\EpProducts\Service\XmlProductReader;
|
||||
use TYPO3\CMS\Core\Log\LogManager;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||
@@ -50,7 +52,7 @@ class ImportDatesTask extends AbstractTask
|
||||
$path = $importSource->acquire();
|
||||
|
||||
if ($path !== null) {
|
||||
$importService->import($path);
|
||||
$importService->import(new XmlProductReader($path, GeneralUtility::makeInstance(LogManager::class)->getLogger(XmlProductReader::class)));
|
||||
// Advance the "last imported" bookkeeping only after a successful
|
||||
// import, so a crashed import retries on the next run.
|
||||
$importSource->markImported($path);
|
||||
|
||||
Reference in new issue
Block a user