*/ private array $title; /** @var array */ private array $names; /** @var array */ private array $common; /** * @param array $title * @param array $names * @param array $common */ public function __construct(array $title = [], array $names = [], array $common = []) { $this->title = self::filterMapping($title, 'title'); $this->names = self::filterMapping($names, 'names'); $this->common = self::filterMapping($common, 'common'); } /** * @param array $config */ public static function fromArray(array $config): self { $title = []; if (array_key_exists('title', $config)) { if (!is_array($config['title'])) { throw new InvalidArgumentException('Overrides "title" section must be a map.'); } $title = self::normalizeSection($config['title'], 'title'); } $names = []; if (array_key_exists('names', $config)) { if (!is_array($config['names'])) { throw new InvalidArgumentException('Overrides "names" section must be a map.'); } $names = self::normalizeSection($config['names'], 'names'); } $common = []; if (array_key_exists('common', $config)) { if (!is_array($config['common'])) { throw new InvalidArgumentException('Overrides "common" section must be a map.'); } $common = self::normalizeSection($config['common'], 'common'); } return new self($title, $names, $common); } public static function fromIniString(string $ini): self { return self::fromString($ini); } public static function fromIniFile(string $path): self { return self::fromFile($path); } public static function fromString(string $contents): self { return self::fromArray(self::parseOverrides($contents)); } public static function fromFile(string $path): self { if (!is_file($path)) { throw new InvalidArgumentException('Overrides INI file not found: ' . $path); } $contents = file_get_contents($path); if ($contents === false) { throw new InvalidArgumentException('Overrides INI file not readable: ' . $path); } return self::fromString($contents); } /** * @return array */ public function titleMap(): array { return array_merge($this->common, $this->title); } /** * @return array */ public function namesMap(): array { return array_merge($this->common, $this->names); } /** * @param array $mapping * @return array */ private static function filterMapping(array $mapping, string $section): array { $result = []; foreach ($mapping as $key => $value) { if (!is_string($key) || !is_string($value)) { throw new InvalidArgumentException( 'Overrides "' . $section . '" section must be a string-to-string map.' ); } $normalizedKey = self::normalizeKey($key); if ($normalizedKey === '') { continue; } $normalizedValue = self::normalizeValue($value); if ($normalizedValue === '') { continue; } $result[$normalizedKey] = $normalizedValue; } return $result; } /** * @param array $section * @return array */ private static function normalizeSection(array $section, string $name): array { $result = []; foreach ($section as $key => $value) { if (is_int($key)) { if (!is_string($value)) { throw new InvalidArgumentException( 'Overrides "' . $name . '" section must contain strings.' ); } $canonical = self::normalizeValue($value); if ($canonical === '') { continue; } $normalizedKey = self::normalizeKey($canonical); if ($normalizedKey === '') { continue; } $result[$normalizedKey] = $canonical; continue; } if (!is_string($key) || !is_string($value)) { throw new InvalidArgumentException( 'Overrides "' . $name . '" section must be a string-to-string map.' ); } $normalizedKey = self::normalizeKey($key); if ($normalizedKey === '') { continue; } $normalizedValue = self::normalizeValue($value); if ($normalizedValue === '') { continue; } $result[$normalizedKey] = $normalizedValue; } return $result; } /** * @return array{title: array, names: array} */ private static function parseOverrides(string $ini): array { $lines = preg_split('/\R/u', $ini); if ($lines === false) { return ['title' => [], 'names' => []]; } $section = null; $title = []; $names = []; $common = []; foreach ($lines as $line) { $trimmed = trim($line); if ($trimmed === '') { continue; } $first = $trimmed[0]; if ($first === ';' || $first === '#') { continue; } if (preg_match('/^\[(.+)\]$/u', $trimmed, $match) === 1) { $name = strtolower(trim($match[1])); if ($name === 'title' || $name === 'names' || $name === 'common') { $section = $name; } else { $section = null; } continue; } if ($section === null) { continue; } $canonical = self::normalizeValue($trimmed); $normalizedKey = self::normalizeKey($canonical); if ($normalizedKey === '') { continue; } if ($section === 'title') { $title[$normalizedKey] = $canonical; } elseif ($section === 'names') { $names[$normalizedKey] = $canonical; } else { $common[$normalizedKey] = $canonical; } } return ['title' => $title, 'names' => $names, 'common' => $common]; } private static function normalizeKey(string $key): string { $trimmed = trim($key); if ($trimmed === '') { return ''; } $collapsed = preg_replace('/\s+/u', ' ', $trimmed); return $collapsed ?? $trimmed; } private static function normalizeValue(string $value): string { $trimmed = trim($value); if ($trimmed === '') { return ''; } $collapsed = preg_replace('/\s+/u', ' ', $trimmed); return $collapsed ?? $trimmed; } }