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); |
|||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
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); |
|||
0 ignored issues
–
show
It seems like
$simple_parsed can also be of type false ; however, parameter $simple_parsed of Ini\Parser::parseSections() does only seem to accept array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
153 | |||||
154 | 1 | return $this->parseKeys($inheritance_parsed); |
|||
0 ignored issues
–
show
|
|||||
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
|
|||||
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 |
||||
0 ignored issues
–
show
Unused Code
Comprehensibility
introduced
by
39% of this comment could be valid code. Did you maybe forget this after debugging?
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it. The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production. This check looks for comments that seem to be mostly valid code and reports them. ![]() |
|||||
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)) { |
|||
0 ignored issues
–
show
It seems like
$current can also be of type ArrayObject ; however, parameter $search of array_key_exists() does only seem to accept array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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; |
|||
0 ignored issues
–
show
Are you sure
$current of type ArrayObject|array|mixed can be used in concatenation ?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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
|
|||||
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 |
||||
0 ignored issues
–
show
Unused Code
Comprehensibility
introduced
by
38% of this comment could be valid code. Did you maybe forget this after debugging?
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it. The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production. This check looks for comments that seem to be mostly valid code and reports them. ![]() |
|||||
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 |