nestSeparator = $separator; return $this; } /** * Get nest separator. * * @return string */ public function getNestSeparator() { return $this->nestSeparator; } /** * Marks whether sections should be processed. * When sections are not processed,section names are stripped and section * values are merged * * @see https://www.php.net/parse_ini_file * @param bool $processSections * @return $this */ public function setProcessSections($processSections) { $this->processSections = (bool) $processSections; return $this; } /** * Get if sections should be processed * When sections are not processed,section names are stripped and section * values are merged * * @see https://www.php.net/parse_ini_file * @return bool */ public function getProcessSections() { return $this->processSections; } /** * Set whether boolean, null, and integer values should be returned as their proper types. * When set to false, all values will be returned as strings. * * @see https://www.php.net/parse_ini_file */ public function setTypedMode(bool $typedMode): self { $this->typedMode = $typedMode; return $this; } /** * Get whether boolean, null, and integer values should be returned as their proper types. * When set to false, all values will be returned as strings. * * @see https://www.php.net/parse_ini_file */ public function getTypedMode(): bool { return $this->typedMode; } /** * Get the scanner-mode constant value to be used with the built-in parse_ini_file function. * Either INI_SCANNER_NORMAL or INI_SCANNER_TYPED depending on $typedMode. * * @see https://www.php.net/parse_ini_file */ public function getScannerMode(): int { return $this->getTypedMode() ? INI_SCANNER_TYPED : INI_SCANNER_NORMAL; } /** * fromFile(): defined by Reader interface. * * @see ReaderInterface::fromFile() * @param string $filename * @return array * @throws Exception\RuntimeException */ public function fromFile($filename) { if (! is_file($filename) || ! is_readable($filename)) { throw new Exception\RuntimeException(sprintf( "File '%s' doesn't exist or not readable", $filename )); } $this->directory = dirname($filename); set_error_handler( function ($error, $message = '') use ($filename) { throw new Exception\RuntimeException( sprintf('Error reading INI file "%s": %s', $filename, $message), $error ); }, E_WARNING ); $ini = parse_ini_file($filename, $this->getProcessSections(), $this->getScannerMode()); restore_error_handler(); return $this->process($ini); } /** * fromString(): defined by Reader interface. * * @param string $string * @return array|bool * @throws Exception\RuntimeException */ public function fromString($string) { if (empty($string)) { return []; } $this->directory = null; set_error_handler( function ($error, $message = '') { throw new Exception\RuntimeException( sprintf('Error reading INI string: %s', $message), $error ); }, E_WARNING ); $ini = parse_ini_string($string, $this->getProcessSections(), $this->getScannerMode()); restore_error_handler(); return $this->process($ini); } /** * Process data from the parsed ini file. * * @param array $data * @return array */ protected function process(array $data) { $config = []; foreach ($data as $section => $value) { if (is_array($value)) { if (strpos($section, $this->nestSeparator) !== false) { $sections = explode($this->nestSeparator, $section); $config = array_merge_recursive($config, $this->buildNestedSection($sections, $value)); } else { $config[$section] = $this->processSection($value); } } else { $this->processKey($section, $value, $config); } } return $config; } /** * Process a nested section * * @param array $sections * @param mixed $value * @return array */ private function buildNestedSection($sections, $value) { if (! $sections) { return $this->processSection($value); } $nestedSection = []; $first = array_shift($sections); $nestedSection[$first] = $this->buildNestedSection($sections, $value); return $nestedSection; } /** * Process a section. * * @param array $section * @return array */ protected function processSection(array $section) { $config = []; foreach ($section as $key => $value) { $this->processKey($key, $value, $config); } return $config; } /** * Process a key. * * @param string $key * @param string $value * @param array $config * @return array * @throws Exception\RuntimeException */ protected function processKey($key, $value, array &$config) { if (strpos($key, $this->nestSeparator) !== false) { $pieces = explode($this->nestSeparator, $key, 2); if ($pieces[0] === '' || $pieces[1] === '') { throw new Exception\RuntimeException(sprintf('Invalid key "%s"', $key)); } if (! isset($config[$pieces[0]])) { if ($pieces[0] === '0' && ! empty($config)) { $config = [$pieces[0] => $config]; } else { $config[$pieces[0]] = []; } } elseif (! is_array($config[$pieces[0]])) { throw new Exception\RuntimeException( sprintf('Cannot create sub-key for "%s", as key already exists', $pieces[0]) ); } $this->processKey($pieces[1], $value, $config[$pieces[0]]); } else { if ($key === '@include') { if ($this->directory === null) { throw new Exception\RuntimeException('Cannot process @include statement for a string config'); } $reader = clone $this; $include = $reader->fromFile($this->directory . '/' . $value); $config = array_replace_recursive($config, $include); } else { $config[$key] = $value; } } } }