Issues (9)

src/Parser.php (2 issues)

1
<?php
2
3
namespace Ini;
4
5
use ArrayObject;
6
use LogicException;
7
use InvalidArgumentException;
8
9
/**
10
 * Class Parser
11
 *
12
 * @package Ini
13
 */
14
class Parser
15
{
16
    /**
17
     * Filename of our .ini file.
18
     *
19
     * @var string
20
     */
21
    protected $ini_content;
22
23
    /**
24
     * Enable/disable property nesting feature
25
     *
26
     * @var boolean
27
     */
28
    public $property_nesting = true;
29
30
    /**
31
     * Enable/disable parametric value parsing
32
     *
33
     * @var boolean
34
     */
35
    public $parametric_parsing = false;
36
37
    /**
38
     * Normal: 0
39
     * Raw: 1
40
     * Typed: 2
41
     */
42
    public $ini_parse_option = 0;
43
44
    /**
45
     * Separator in case of multiple values
46
     *
47
     * @var string
48
     */
49
    public $multi_value_separator = '|';
50
51
    /**
52
     * Use ArrayObject to allow array work as object (true) or use native arrays (false)
53
     *
54
     * @var boolean
55
     */
56
    public $use_array_object = true;
57
58
    /**
59
     * Include original sections (pre-inherit names) on the final output
60
     *
61
     * @var boolean
62
     */
63
    public $include_original_sections = false;
64
65
    /**
66
     * If set to true, it will consider the passed parameter as string
67
     *
68
     * @var bool
69
     */
70
    public $treat_ini_string = false;
71
72
    /**
73
     * Disable array literal parsing
74
     */
75
    const NO_PARSE = 0;
76
77
    /**
78
     * Parse simple arrays using regex (ex: [a,b,c,...])
79
     */
80
    const PARSE_SIMPLE = 1;
81
82
    /**
83
     * Parse array literals using JSON, allowing advanced features like
84
     * dictionaries, array nesting, etc.
85
     */
86
    const PARSE_JSON = 2;
87
88
    /**
89
     * Normal: 0
90
     * Raw: 1
91
     * Typed: 2
92
     */
93
    const INI_PARSE_OPTION = 0;
94
95
    /**
96
     * Array literals parse mode
97
     *
98
     * @var int
99
     */
100
    public $array_literals_behavior = self::PARSE_SIMPLE;
101
102
    /**
103
     * Parser constructor.
104
     *
105
     * @param string $iniContent File path or the ini string
106
     */
107 23
    public function __construct(string $iniContent = null)
108
    {
109 23
        if ($iniContent !== null) {
110 20
            $this->setIniContent($iniContent);
111
        }
112 22
    }
113
114
    /**
115
     * Parses an INI file
116
     *
117
     * @param string $iniContent
118
     *
119
     * @return mixed
120
     */
121 21
    public function parse(string $iniContent = null)
122
    {
123 21
        if ($iniContent !== null) {
124 1
            $this->setIniContent($iniContent);
125
        }
126
127 21
        if (empty($this->ini_content)) {
128 1
            throw new LogicException("Need ini content to parse.");
129
        }
130
131 20
        if ($this->treat_ini_string) {
132 1
            $simple_parsed = parse_ini_string($this->ini_content, true, $this->ini_parse_option);
133
        } else {
134 19
            $simple_parsed = parse_ini_file($this->ini_content, true, $this->ini_parse_option);
135
        }
136
137 20
        $inheritance_parsed = $this->parseSections($simple_parsed);
138
139 19
        return $this->parseKeys($inheritance_parsed);
140
    }
141
142
    /**
143
     * Parses a string with INI contents
144
     *
145
     * @param string $src
146
     *
147
     * @return array
148
     */
149 1
    public function process(string $src)
150
    {
151 1
        $simple_parsed = parse_ini_string($src, true, $this->ini_parse_option);
152 1
        $inheritance_parsed = $this->parseSections($simple_parsed);
153
154 1
        return $this->parseKeys($inheritance_parsed);
155
    }
156
157
    /**
158
     * @param string $ini_content
159
     *
160
     * @return \Ini\Parser
161
     * @throws \InvalidArgumentException
162
     */
163 21
    public function setIniContent(string $ini_content)
164
    {
165
        // If the parsed parameter is to be treated as string instead of file
166 21
        if ($this->treat_ini_string) {
167 1
            $this->ini_content = $ini_content;
168
        } else {
169 20
            if (!file_exists($ini_content) || !is_readable($ini_content)) {
170 1
                throw new InvalidArgumentException("The file '{$ini_content}' cannot be opened.");
171
            }
172
173 19
            $this->ini_content = $ini_content;
174
        }
175
176 20
        return $this;
177
    }
178
179
    /**
180
     * Parse sections and inheritance.
181
     *
182
     * @param  array $simple_parsed
183
     *
184
     * @return array  Parsed sections
185
     */
186 21
    private function parseSections(array $simple_parsed)
187
    {
188
        // do an initial pass to gather section names
189 21
        $sections = [];
190 21
        $globals = [];
191 21
        foreach ($simple_parsed as $k => $v) {
192 21
            if (is_array($v)) {
193
                // $k is a section name
194 20
                $sections[$k] = $v;
195
            } else {
196 21
                $globals[$k] = $v;
197
            }
198
        }
199
200
        // now for each section, see if it uses inheritance
201 21
        $output_sections = [];
202 21
        foreach ($sections as $k => $v) {
203 20
            $sects = array_map('trim', array_reverse(explode(':', $k)));
204 20
            $root = array_pop($sects);
205 20
            $arr = $v;
206 20
            foreach ($sects as $s) {
207 11
                if ($s === '^') {
208 3
                    $arr = array_merge($globals, $arr);
209 11
                } elseif (array_key_exists($s, $output_sections)) {
210 9
                    $arr = array_merge($output_sections[$s], $arr);
211 2
                } elseif (array_key_exists($s, $sections)) {
212 1
                    $arr = array_merge($sections[$s], $arr);
213
                } else {
214 11
                    throw new \UnexpectedValueException("IniParser: In file '{$this->ini_content}', section '{$root}': Cannot inherit from unknown section '{$s}'");
0 ignored issues
show
This line exceeds maximum limit of 140 characters; contains 164 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
215
                }
216
            }
217
218 19
            if ($this->include_original_sections) {
219
                $output_sections[$k] = $v;
220
            }
221 19
            $output_sections[$root] = $arr;
222
        }
223
224
225 20
        return $globals + $output_sections;
226
    }
227
228
    /**
229
     * @param array $arr
230
     *
231
     * @return mixed
232
     */
233 20
    private function parseKeys(array $arr)
234
    {
235 20
        $output = $this->getArrayValue();
236 20
        $append_regex = '/\s*\+\s*$/';
237 20
        foreach ($arr as $k => $v) {
238 20
            if (is_array($v) && false === strpos($k, '.')) {
239
                // this element represents a section; recursively parse the value
240 18
                $output[$k] = $this->parseKeys($v);
241
            } else {
242
                // if the key ends in a +, it means we should append to the previous value, if applicable
243 20
                $append = false;
244 20
                if (preg_match($append_regex, $k)) {
245 3
                    $k = preg_replace($append_regex, '', $k);
246 3
                    $append = true;
247
                }
248
249
                // transform "a.b.c = x" into $output[a][b][c] = x
250 20
                $current = &$output;
251
252 20
                $path = $this->property_nesting ? explode('.', $k) : [$k];
253 20
                while (($current_key = array_shift($path)) !== null) {
254 20
                    if ('string' === gettype($current)) {
255
                        $current = [$current];
256
                    }
257
258 20
                    if (!array_key_exists($current_key, $current)) {
259 20
                        if (!empty($path)) {
260 7
                            $current[$current_key] = $this->getArrayValue();
261
                        } else {
262 20
                            $current[$current_key] = null;
263
                        }
264
                    }
265 20
                    $current = &$current[$current_key];
266
                }
267
268
                // parse value
269 20
                $value = $v;
270 20
                if (!is_array($v)) {
271 19
                    $value = $this->parseValue($v);
272
                }
273
274 20
                if ($append && $current !== null) {
275 3
                    if (is_array($value)) {
276 3
                        if (!is_array($current)) {
277
                            throw new LogicException("Cannot append array to inherited value '{$k}'");
278
                        }
279 3
                        $value = array_merge($current, $value);
280 3
                        $value = array_map([$this, 'parseParametricValue'], $value);
281
                    } else {
282 3
                        $value = $current . $value;
283
                    }
284
                }
285
286 20
                $current = $this->parseParametricValue($value);
287
            }
288
        }
289
290 20
        return $output;
291
    }
292
293
    /**
294
     * Parses the parametric value to multiple parameters
295
     *
296
     * @param $value
297
     * // todo is array or string?
298
     * @return array|string
299
     */
300 20
    protected function parseParametricValue($value)
301
    {
302
        // If parametric parsing isn't turned on or value has no parameters
303 20
        if (!$this->parametric_parsing || !is_string($value) || strpos($value, '=') === false) {
304 19
            return $value;
305
        }
306
307
        // As there could be multiple parameters separated by spaces
308 1
        $parameters = preg_split('/\s+/', $value);
309
310 1
        $parsedValue = [];
311 1
        foreach ($parameters as $parameter) {
312 1
            list($parameterKey, $parameterValue) = explode('=', $parameter);
313
            // todo simplify
314 1
            $parsedValue[$parameterKey] = strpos($parameterValue, $this->multi_value_separator) !== false ? explode($this->multi_value_separator, $parameterValue) : $parameterValue;
0 ignored issues
show
This line exceeds maximum limit of 140 characters; contains 181 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
315
        }
316
317 1
        return $parsedValue;
318
    }
319
320
    /**
321
     * Parses and formats the value in a key-value pair
322
     *
323
     * @param string $value
324
     *
325
     * @return mixed
326
     */
327 19
    protected function parseValue(string $value)
328
    {
329 19
        switch ($this->array_literals_behavior) {
330 19
            case self::PARSE_JSON:
331 1
                if (in_array(substr($value, 0, 1), ['[', '{']) && in_array(substr($value, -1), [']', '}'])) {
332 1
                    if (defined('JSON_BIGINT_AS_STRING')) {
333 1
                        $output = json_decode($value, true, 512, JSON_BIGINT_AS_STRING);
334
                    } else {
335
                        $output = json_decode($value, true);
336
                    }
337
338 1
                    if ($output !== null) {
339 1
                        return $output;
340
                    }
341
                }
342
            // fallthrough
343
            // try regex parser for simple estructures not JSON-compatible (ex: colors = [blue, green, red])
344 18
            case self::PARSE_SIMPLE:
345
                // if the value looks like [a,b,c,...], interpret as array
346 18
                if (preg_match('/^\[\s*.*?(?:\s*,\s*.*?)*\s*\]$/', trim($value))) {
347 7
                    return array_map('trim', explode(',', trim(trim($value), '[]')));
348
                }
349 18
                break;
350
        }
351
352 18
        return $value;
353
    }
354
355
    /**
356
     * @param array $array
357
     *
358
     * @return array|\ArrayObject
359
     */
360 20
    protected function getArrayValue(array $array = [])
361
    {
362 20
        if ($this->use_array_object) {
363 20
            return new ArrayObject($array, ArrayObject::ARRAY_AS_PROPS);
364
        } else {
365 1
            return $array;
366
        }
367
    }
368
}
369