Completed
Push — master ( f5d695...5378a6 )
by Mohamed
01:54
created

Parser   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 352
Duplicated Lines 0 %

Test Coverage

Coverage 96.3%

Importance

Changes 0
Metric Value
wmc 51
dl 0
loc 352
ccs 104
cts 108
cp 0.963
rs 8.3206
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
D parseSections() 0 40 9
A getArrayValue() 0 6 2
C parseKeys() 0 58 15
C parseValue() 0 26 8
A __construct() 0 4 2
B parseParametricValue() 0 18 6
A setIniContent() 0 14 4
A process() 0 6 1
A parse() 0 19 4

How to fix   Complexity   

Complex Class

Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.

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
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 ignore-type  annotation

137
        $inheritance_parsed = $this->parseSections(/** @scrutinizer ignore-type */ $simple_parsed);
Loading history...
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
Bug introduced by
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 ignore-type  annotation

152
        $inheritance_parsed = $this->parseSections(/** @scrutinizer ignore-type */ $simple_parsed);
Loading history...
153
154 1
        return $this->parseKeys($inheritance_parsed);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parseKeys($inheritance_parsed) also could return the type ArrayObject which is incompatible with the documented return type array.
Loading history...
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
Coding Style introduced by
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
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.

Loading history...
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
Bug introduced by
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 ignore-type  annotation

258
                    if (!array_key_exists($current_key, /** @scrutinizer ignore-type */ $current)) {
Loading history...
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
Bug introduced by
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 ignore-type  annotation

282
                        $value = /** @scrutinizer ignore-type */ $current . $value;
Loading history...
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
Coding Style introduced by
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
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.

Loading history...
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