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