['indikator', 'indikatoren'], 'indikatoren' => ['indikator', 'indikatoren'], 'reagenz' => ['reagenz', 'reagenzien'], 'reagenzien' => ['reagenz', 'reagenzien'], 'ersatzteil' => ['ersatzteil', 'ersatzteile'], 'ersatzteile' => ['ersatzteil', 'ersatzteile'], 'service set' => ['service set', 'serviceset', 'service-set'], 'serviceset' => ['service set', 'serviceset', 'service-set'], 'service-set' => ['service set', 'serviceset', 'service-set'], ]; /** * @param array $config */ public function __construct( private array $config = [], private readonly ?DomainVocabularyConfig $vocabulary = null, ) { } public function getTopProductLogLimit(): int { return $this->int('top_product_log_limit', 3, 0); } /** @return string[] */ public function getDeviceFocusKeywords(): array { return $this->stringList('device_focus_keywords', $this->vocabularyView('shop.device_focus', self::DEVICE_FOCUS_KEYWORDS)); } /** @return string[] */ public function getAccessoryFocusKeywords(): array { return $this->stringList('accessory_focus_keywords', $this->vocabularyView('shop.accessory_focus', self::ACCESSORY_FOCUS_KEYWORDS)); } /** @return array */ public function getAccessoryFocusVariantMap(): array { return $this->stringListMap('accessory_focus_variant_map', $this->vocabularyMap('shop.accessory_focus_variants', self::ACCESSORY_FOCUS_VARIANT_MAP)); } /** @return string[] */ public function getDeviceQueryKeywords(): array { return $this->stringList('device_query_keywords', $this->vocabularyView('shop.device_query', self::DEVICE_QUERY_KEYWORDS)); } /** @return string[] */ public function getAccessoryQueryKeywords(): array { return $this->stringList('accessory_query_keywords', $this->vocabularyView('shop.accessory_query', self::ACCESSORY_QUERY_KEYWORDS)); } /** @return string[] */ public function getAccessoryProductKeywords(): array { return $this->stringList('accessory_product_keywords', $this->vocabularyView('shop.accessory_product', self::ACCESSORY_PRODUCT_KEYWORDS)); } /** @return string[] */ public function getDeviceProductKeywords(): array { return $this->stringList('device_product_keywords', $this->vocabularyView('shop.device_product', self::DEVICE_PRODUCT_KEYWORDS)); } public function getExactProductNumberPhraseScore(): int { return $this->int('scores.exact_product_number_phrase', 160); } public function getExactProductNamePhraseScore(): int { return $this->int('scores.exact_product_name_phrase', 90); } public function getExactManufacturerMatchScore(): int { return $this->int('scores.exact_manufacturer_match', 40); } public function getBrandContainedInNameScore(): int { return $this->int('scores.brand_contained_in_name', 20); } public function getNameTokenOverlapWeight(): int { return $this->int('scores.name_token_overlap_weight', 6); } public function getProductNumberTokenOverlapWeight(): int { return $this->int('scores.product_number_token_overlap_weight', 10); } public function getCorpusTokenOverlapWeight(): int { return $this->int('scores.corpus_token_overlap_weight', 2); } public function getNameNumberOverlapWeight(): int { return $this->int('scores.name_number_overlap_weight', 18); } public function getProductNumberNumberOverlapWeight(): int { return $this->int('scores.product_number_number_overlap_weight', 28); } public function getCorpusNumberOverlapWeight(): int { return $this->int('scores.corpus_number_overlap_weight', 8); } public function getSizeMatchScore(): int { return $this->int('scores.size_match', 12); } public function getAvailabilityBonusScore(): int { return $this->int('scores.availability_bonus', 1); } public function getDeviceQueryDeviceProductBonus(): int { return $this->int('scores.device_query_device_product_bonus', 60); } public function getDeviceQueryAccessoryPenalty(): int { return $this->int('scores.device_query_accessory_penalty', 120); } public function getAccessoryQueryAccessoryProductBonus(): int { return $this->int('scores.accessory_query_accessory_product_bonus', 30); } public function getAccessoryQueryDeviceProductBonus(): int { return $this->int('scores.accessory_query_device_product_bonus', 10); } public function getContainsDigitPattern(): string { return $this->string('patterns.contains_digit', '/\d/u'); } public function getMatchingCleanupPattern(): string { return $this->string('patterns.matching_cleanup', '/[^\p{L}\p{N}]+/u'); } public function getWhitespaceCollapsePattern(): string { return $this->string('patterns.whitespace_collapse', '/\s+/u'); } public function getTokenSplitPattern(): string { return $this->string('patterns.token_split', '/[^\p{L}\p{N}]+/u'); } public function wrapWithPaddingSpaces(string $value): string { return $this->string('padding.prefix', ' ') . trim($value) . $this->string('padding.suffix', ' '); } /** @return string[] */ public function getPriceNormalizationSearch(): array { return $this->stringList('price.normalization_search', ['€', ' ', '.']); } /** @return string[] */ public function getPriceNormalizationReplace(): array { return $this->stringList('price.normalization_replace', ['', '', ''], true, ['', '', '']); } public function getPrimaryCustomFieldKey(): string { return $this->string('custom_fields.primary', 'migration_Backup_product_attr1'); } public function getSecondaryCustomFieldKey(): string { return $this->string('custom_fields.secondary', 'migration_Backup_product_attr2'); } public function getUseCasesCustomFieldKey(): string { return $this->string('custom_fields.use_cases', 'migration_Backup_product_attr4'); } public function getLanguagesCustomFieldKey(): string { return $this->string('custom_fields.languages', 'migration_Backup_product_attr5'); } public function getPrimarySecondarySeparator(): string { return $this->string('text.primary_secondary_separator', ': '); } public function getUseCasesLabel(): string { return $this->string('text.use_cases_label', 'Einsatzgebiete: '); } public function getLanguagesLabel(): string { return $this->string('text.languages_label', 'Sprachen: '); } public function getCustomFieldJoinSeparator(): string { return $this->string('text.custom_field_join_separator', ' | '); } public function getDescriptionEmptyLinePattern(): string { return $this->string('description.empty_line_pattern', '/^[ \t]*\R/m'); } public function getDescriptionWhitespaceCleanupPattern(): string { return $this->string('description.whitespace_cleanup_pattern', '/[ \t]{2,}/'); } public function getDescriptionMaxLength(): int { return $this->int('description.max_length', 1500, 0); } public function getPriceDecimals(): int { return $this->int('price.decimals', 2, 0); } public function getPriceDecimalSeparator(): string { return $this->string('price.decimal_separator', ','); } public function getPriceThousandsSeparator(): string { return $this->string('price.thousands_separator', '.'); } public function getPriceSuffix(): string { return $this->string('price.suffix', ' €'); } public function buildRelativeSeoUrl(string $path): string { return $this->string('seo.relative_prefix', '/') . ltrim($path, '/'); } public function getAvailableHighlightLabel(): string { return $this->string('highlight.available_label', 'Verfügbar'); } public function getUnavailableHighlightLabel(): string { return $this->string('highlight.unavailable_label', 'Nicht verfügbar'); } public function getProductNumberHighlightPrefix(): string { return $this->string('highlight.product_number_prefix', 'Produktnummer: '); } public function getMissingProductImagePlaceholder(): string { return $this->string('image.missing_placeholder', 'no-image'); } public function getDeduplicationSeparator(): string { return $this->string('deduplication.separator', '|'); } private function int(string $path, int $default, int $min = PHP_INT_MIN): int { $value = $this->value($path, $default); if (!is_numeric($value)) { return $default; } return max($min, (int) $value); } private function string(string $path, string $default): string { $value = $this->value($path, $default); if (!is_scalar($value)) { return $default; } return (string) $value; } /** * @param string[] $default * @param string[]|null $emptySafeDefault * @return string[] */ /** @return string[] */ private function vocabularyView(string $path, array $fallback): array { return $this->vocabulary?->view($path, $fallback) ?? $fallback; } /** @return array */ private function vocabularyMap(string $path, array $fallback): array { return $this->vocabulary?->map($path, $fallback) ?? $fallback; } private function stringList(string $path, array $default, bool $allowEmptyStrings = false, ?array $emptySafeDefault = null): array { $value = $this->value($path, $default); if (!is_array($value)) { return $emptySafeDefault ?? $default; } $out = []; foreach ($value as $item) { if (!is_scalar($item)) { continue; } $item = (string) $item; if (!$allowEmptyStrings) { $item = trim($item); } if (!$allowEmptyStrings && $item === '') { continue; } if ($allowEmptyStrings || !in_array($item, $out, true)) { $out[] = $item; } } if ($out === [] && !$allowEmptyStrings) { return $emptySafeDefault ?? $default; } return $out; } /** * @param array $default * @return array */ private function stringListMap(string $path, array $default): array { $value = $this->value($path, $default); if (!is_array($value)) { return $default; } $out = []; foreach ($value as $key => $items) { if (!is_string($key) || !is_array($items)) { continue; } $cleanKey = trim($key); if ($cleanKey === '') { continue; } $cleanItems = []; foreach ($items as $item) { if (!is_scalar($item)) { continue; } $item = trim((string) $item); if ($item === '') { continue; } if (!in_array($item, $cleanItems, true)) { $cleanItems[] = $item; } } if ($cleanItems !== []) { $out[$cleanKey] = $cleanItems; } } return $out !== [] ? $out : $default; } private function value(string $path, mixed $default): mixed { $current = $this->config; foreach (explode('.', $path) as $segment) { if (!is_array($current) || !array_key_exists($segment, $current)) { return $default; } $current = $current[$segment]; } return $current; } }