1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Config\Reader; 11 12use Zend\Config\Exception; 13 14/** 15 * INI config reader. 16 */ 17class Ini implements ReaderInterface 18{ 19 /** 20 * Separator for nesting levels of configuration data identifiers. 21 * 22 * @var string 23 */ 24 protected $nestSeparator = '.'; 25 26 /** 27 * Directory of the file to process. 28 * 29 * @var string 30 */ 31 protected $directory; 32 33 /** 34 * Set nest separator. 35 * 36 * @param string $separator 37 * @return self 38 */ 39 public function setNestSeparator($separator) 40 { 41 $this->nestSeparator = $separator; 42 return $this; 43 } 44 45 /** 46 * Get nest separator. 47 * 48 * @return string 49 */ 50 public function getNestSeparator() 51 { 52 return $this->nestSeparator; 53 } 54 55 /** 56 * fromFile(): defined by Reader interface. 57 * 58 * @see ReaderInterface::fromFile() 59 * @param string $filename 60 * @return array 61 * @throws Exception\RuntimeException 62 */ 63 public function fromFile($filename) 64 { 65 if (!is_file($filename) || !is_readable($filename)) { 66 throw new Exception\RuntimeException(sprintf( 67 "File '%s' doesn't exist or not readable", 68 $filename 69 )); 70 } 71 72 $this->directory = dirname($filename); 73 74 set_error_handler( 75 function ($error, $message = '') use ($filename) { 76 throw new Exception\RuntimeException( 77 sprintf('Error reading INI file "%s": %s', $filename, $message), 78 $error 79 ); 80 }, 81 E_WARNING 82 ); 83 $ini = parse_ini_file($filename, true); 84 restore_error_handler(); 85 86 return $this->process($ini); 87 } 88 89 /** 90 * fromString(): defined by Reader interface. 91 * 92 * @param string $string 93 * @return array|bool 94 * @throws Exception\RuntimeException 95 */ 96 public function fromString($string) 97 { 98 if (empty($string)) { 99 return array(); 100 } 101 $this->directory = null; 102 103 set_error_handler( 104 function ($error, $message = '') { 105 throw new Exception\RuntimeException( 106 sprintf('Error reading INI string: %s', $message), 107 $error 108 ); 109 }, 110 E_WARNING 111 ); 112 $ini = parse_ini_string($string, true); 113 restore_error_handler(); 114 115 return $this->process($ini); 116 } 117 118 /** 119 * Process data from the parsed ini file. 120 * 121 * @param array $data 122 * @return array 123 */ 124 protected function process(array $data) 125 { 126 $config = array(); 127 128 foreach ($data as $section => $value) { 129 if (is_array($value)) { 130 if (strpos($section, $this->nestSeparator) !== false) { 131 $sections = explode($this->nestSeparator, $section); 132 $config = array_merge_recursive($config, $this->buildNestedSection($sections, $value)); 133 } else { 134 $config[$section] = $this->processSection($value); 135 } 136 } else { 137 $this->processKey($section, $value, $config); 138 } 139 } 140 141 return $config; 142 } 143 144 /** 145 * Process a nested section 146 * 147 * @param array $sections 148 * @param mixed $value 149 * @return array 150 */ 151 private function buildNestedSection($sections, $value) 152 { 153 if (count($sections) == 0) { 154 return $this->processSection($value); 155 } 156 157 $nestedSection = array(); 158 159 $first = array_shift($sections); 160 $nestedSection[$first] = $this->buildNestedSection($sections, $value); 161 162 return $nestedSection; 163 } 164 165 /** 166 * Process a section. 167 * 168 * @param array $section 169 * @return array 170 */ 171 protected function processSection(array $section) 172 { 173 $config = array(); 174 175 foreach ($section as $key => $value) { 176 $this->processKey($key, $value, $config); 177 } 178 179 return $config; 180 } 181 182 /** 183 * Process a key. 184 * 185 * @param string $key 186 * @param string $value 187 * @param array $config 188 * @return array 189 * @throws Exception\RuntimeException 190 */ 191 protected function processKey($key, $value, array &$config) 192 { 193 if (strpos($key, $this->nestSeparator) !== false) { 194 $pieces = explode($this->nestSeparator, $key, 2); 195 196 if (!strlen($pieces[0]) || !strlen($pieces[1])) { 197 throw new Exception\RuntimeException(sprintf('Invalid key "%s"', $key)); 198 } elseif (!isset($config[$pieces[0]])) { 199 if ($pieces[0] === '0' && !empty($config)) { 200 $config = array($pieces[0] => $config); 201 } else { 202 $config[$pieces[0]] = array(); 203 } 204 } elseif (!is_array($config[$pieces[0]])) { 205 throw new Exception\RuntimeException( 206 sprintf('Cannot create sub-key for "%s", as key already exists', $pieces[0]) 207 ); 208 } 209 210 $this->processKey($pieces[1], $value, $config[$pieces[0]]); 211 } else { 212 if ($key === '@include') { 213 if ($this->directory === null) { 214 throw new Exception\RuntimeException('Cannot process @include statement for a string config'); 215 } 216 217 $reader = clone $this; 218 $include = $reader->fromFile($this->directory . '/' . $value); 219 $config = array_replace_recursive($config, $include); 220 } else { 221 $config[$key] = $value; 222 } 223 } 224 } 225} 226