Parser   F
last analyzed

Complexity

Total Complexity 101

Size/Duplication

Total Lines 541
Duplicated Lines 4.07 %

Coupling/Cohesion

Components 5
Dependencies 9

Importance

Changes 0
Metric Value
dl 22
loc 541
rs 2
c 0
b 0
f 0
wmc 101
lcom 5
cbo 9

10 Methods

Rating   Name   Duplication   Size   Complexity  
A isErroneous() 0 4 1
A parse() 0 11 2
F __construct() 22 294 77
B purgeScope() 0 20 11
A getCurrentScope() 0 6 1
A raiseError() 0 14 4
A token() 0 4 1
A getCurrentChar() 0 13 2
A getNextChar() 0 4 1
A rewind() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
namespace PHPDaemon\Config;
3
4
use PHPDaemon\Config\Entry\Generic;
5
use PHPDaemon\Core\Daemon;
6
use PHPDaemon\Core\Debug;
7
use PHPDaemon\Exceptions\InfiniteRecursion;
8
9
/**
10
 * Config parser
11
 *
12
 * @package    Core
13
 * @subpackage Config
14
 *
15
 * @author     Vasily Zorin <[email protected]>
16
 */
17
class Parser
18
{
19
    use \PHPDaemon\Traits\ClassWatchdog;
20
    use \PHPDaemon\Traits\StaticObjectWatchdog;
21
22
    /**
23
     * State: standby
24
     */
25
    const T_ALL = 1;
26
    /**
27
     * State: comment
28
     */
29
    const T_COMMENT = 2;
30
    /**
31
     * State: variable definition block
32
     */
33
    const T_VAR = 3;
34
    /**
35
     * Single-quoted string
36
     */
37
    const T_STRING = 4;
38
39
    /**
40
     * Double-quoted
41
     */
42
    const T_STRING_DOUBLE = 5;
43
44
    /**
45
     * Block
46
     */
47
    const T_BLOCK = 6;
48
49
    /**
50
     * Value defined by constant (keyword) or number
51
     */
52
    const T_CVALUE = 7;
53
54
    /**
55
     * Config file path
56
     * @var string
57
     */
58
    protected $file;
59
60
    /**
61
     * Current line number
62
     * @var number
63
     */
64
    protected $line = 1;
65
66
    /**
67
     * Current column number
68
     * @var number
69
     */
70
    protected $col = 1;
71
72
    /**
73
     * Pointer (current offset)
74
     * @var integer
75
     */
76
    protected $p = 0;
77
78
    /**
79
     * State stack
80
     * @var array
81
     */
82
    protected $state = [];
83
84
    /**
85
     * Target object
86
     * @var object
87
     */
88
    protected $target;
89
90
    /**
91
     * Erroneous?
92
     * @var boolean
93
     */
94
    protected $erroneous = false;
95
96
    /**
97
     * Callbacks
98
     * @var array
99
     */
100
    protected $tokens;
101
102
    /**
103
     * File length
104
     * @var integer
105
     */
106
    protected $length;
107
108
    /**
109
     * Revision
110
     * @var integer
111
     */
112
    protected $revision;
113
114
    /**
115
     * Contents of config file
116
     * @var string
117
     */
118
    protected $data;
119
120
    /**
121
     * Parse stack
122
     * @var array
123
     */
124
    protected static $stack = [];
125
126
    /**
127
     * Erroneous?
128
     * @return boolean
129
     */
130
    public function isErroneous()
131
    {
132
        return $this->erroneous;
133
    }
134
135
    /**
136
     * Parse config file
137
     * @param string  File path
138
     * @param _Object  Target
139
     * @param boolean Included? Default is false
140
     * @return \PHPDaemon\Config\Parser
141
     */
142
    public static function parse($file, $target, $included = false)
143
    {
144
        if (in_array($file, static::$stack)) {
145
            throw new InfiniteRecursion;
146
        }
147
148
        static::$stack[] = $file;
149
        $parser = new static($file, $target, $included);
150
        array_pop(static::$stack);
151
        return $parser;
152
    }
153
154
    /**
155
     * Constructor
156
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
157
     */
158
    protected function __construct($file, $target, $included = false)
159
    {
160
        $this->file = $file;
161
        $this->target = $target;
162
        $this->revision = ++_Object::$lastRevision;
163
        $this->data = file_get_contents($file);
164
165
        if (substr($this->data, 0, 2) === '#!') {
166
            if (!is_executable($file)) {
167
                $this->raiseError('Shebang (#!) detected in the first line, but file hasn\'t +x mode.');
168
                return;
169
            }
170
            $this->data = shell_exec($file);
171
        }
172
173
        $this->data = str_replace("\r", '', $this->data);
174
        $this->length = mb_orig_strlen($this->data);
175
        $this->state[] = [static::T_ALL, $this->target];
176
        $this->tokens = [
177
            static::T_COMMENT => function ($c) {
178
                if ($c === "\n") {
179
                    array_pop($this->state);
180
                }
181
            },
182
            static::T_STRING_DOUBLE => function ($q) {
183
                $str = '';
184
                ++$this->p;
185
186
                for (; $this->p < $this->length; ++$this->p) {
187
                    $c = $this->getCurrentChar();
188
189
                    if ($c === $q) {
190
                        ++$this->p;
191
                        break;
192
                    } elseif ($c === '\\') {
193
                        next:
194
                        $n = $this->getNextChar();
195
                        if ($n === $q) {
196
                            $str .= $q;
197
                            ++$this->p;
198
                        } elseif (ctype_digit($n)) {
199
                            $def = $n;
200
                            ++$this->p;
201 View Code Duplication
                            for (; $this->p < min($this->length, $this->p + 2); ++$this->p) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
202
                                $n = $this->getNextChar();
203
                                if (!ctype_digit($n)) {
204
                                    break;
205
                                }
206
                                $def .= $n;
207
                            }
208
                            $str .= chr((int)$def);
209
                        } elseif (($n === 'x') || ($n === 'X')) {
210
                            $def = $n;
211
                            ++$this->p;
212 View Code Duplication
                            for (; $this->p < min($this->length, $this->p + 2); ++$this->p) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
213
                                $n = $this->getNextChar();
214
                                if (!ctype_xdigit($n)) {
215
                                    break;
216
                                }
217
                                $def .= $n;
218
                            }
219
                            $str .= chr((int)hexdec($def));
220
                        } else {
221
                            $str .= $c;
222
                        }
223
                    } else {
224
                        $str .= $c;
225
                    }
226
                }
227
228
                if ($this->p >= $this->length) {
229
                    $this->raiseError('Unexpected End-Of-File.');
230
                }
231
                return $str;
232
            },
233
            static::T_STRING => function ($q) {
234
                $str = '';
235
                ++$this->p;
236
237
                for (; $this->p < $this->length; ++$this->p) {
238
                    $c = $this->getCurrentChar();
239
240
                    if ($c === $q) {
241
                        ++$this->p;
242
                        break;
243
                    } elseif ($c === '\\') {
244
                        if ($this->getNextChar() === $q) {
245
                            $str .= $q;
246
                            ++$this->p;
247
                        } else {
248
                            $str .= $c;
249
                        }
250
                    } else {
251
                        $str .= $c;
252
                    }
253
                }
254
255
                if ($this->p >= $this->length) {
256
                    $this->raiseError('Unexpected End-Of-File.');
257
                }
258
                return $str;
259
            },
260
            static::T_ALL => function ($c) {
261
                if (ctype_space($c)) {
262
                } elseif ($c === '#') {
263
                    $this->state[] = [static::T_COMMENT];
264
                } elseif ($c === '}') {
265
                    if (sizeof($this->state) > 1) {
266
                        $this->purgeScope($this->getCurrentScope());
267
                        array_pop($this->state);
268
                    } else {
269
                        $this->raiseError('Unexpected \'}\'');
270
                    }
271
                } elseif (ctype_alnum($c) || $c === '\\') {
272
                    $elements = [''];
273
                    $elTypes = [null];
274
                    $i = 0;
275
                    $tokenType = 0;
276
                    $newLineDetected = null;
277
278
                    for (; $this->p < $this->length; ++$this->p) {
279
                        $prePoint = [$this->line, $this->col - 1];
280
                        $c = $this->getCurrentChar();
281
282
                        if (ctype_space($c) || $c === '=' || $c === ',') {
283
                            if ($c === "\n") {
284
                                $newLineDetected = $prePoint;
285
                            }
286
                            if ($elTypes[$i] !== null) {
287
                                ++$i;
288
                                $elTypes[$i] = null;
289
                            }
290
                        } elseif ($c === '\'') {
291
                            if ($elTypes[$i] !== null) {
292
                                $this->raiseError('Unexpected T_STRING.');
293
                            }
294
295
                            $string = $this->token(static::T_STRING, $c);
296
                            --$this->p;
297
298 View Code Duplication
                            if ($elTypes[$i] === null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
299
                                $elements[$i] = $string;
300
                                $elTypes[$i] = static::T_STRING;
301
                            }
302
                        } elseif ($c === '"') {
303
                            if ($elTypes[$i] !== null) {
304
                                $this->raiseError('Unexpected T_STRING_DOUBLE.');
305
                            }
306
307
                            $string = $this->token(static::T_STRING_DOUBLE, $c);
308
                            --$this->p;
309
310 View Code Duplication
                            if ($elTypes[$i] === null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
311
                                $elements[$i] = $string;
312
                                $elTypes[$i] = static::T_STRING_DOUBLE;
313
                            }
314
                        } elseif ($c === '}') {
315
                            $this->raiseError('Unexpected \'}\' instead of \';\' or \'{\'');
316
                        } elseif ($c === ';') {
317
                            if ($newLineDetected) {
318
                                $this->raiseError('Unexpected new-line instead of \';\'', 'notice', $newLineDetected[0],
319
                                    $newLineDetected[1]);
320
                            }
321
                            $tokenType = static::T_VAR;
322
                            break;
323
                        } elseif ($c === '{') {
324
                            $tokenType = static::T_BLOCK;
325
                            break;
326
                        } else {
327
                            if ($elTypes[$i] === static::T_STRING) {
328
                                $this->raiseError('Unexpected T_CVALUE.');
329
                            } else {
330
                                if (!isset($elements[$i])) {
331
                                    $elements[$i] = '';
332
                                }
333
334
                                $elements[$i] .= $c;
335
                                $elTypes[$i] = static::T_CVALUE;
336
                            }
337
                        }
338
                    }
339
                    foreach ($elTypes as $k => $v) {
340
                        if (static::T_CVALUE === $v) {
341
                            if (ctype_digit($elements[$k])) {
342
                                $elements[$k] = (int)$elements[$k];
343
                            } elseif (is_numeric($elements[$k])) {
344
                                $elements[$k] = (float)$elements[$k];
345
                            } else {
346
                                $l = strtolower($elements[$k]);
347
348
                                if (($l === 'true') || ($l === 'on')) {
349
                                    $elements[$k] = true;
350
                                } elseif (($l === 'false') || ($l === 'off')) {
351
                                    $elements[$k] = false;
352
                                } elseif ($l === 'null') {
353
                                    $elements[$k] = null;
354
                                }
355
                            }
356
                        }
357
                    }
358
                    if ($tokenType === 0) {
359
                        $this->raiseError('Expected \';\' or \'{\'');
360
                    } elseif ($tokenType === static::T_VAR) {
361
                        $name = str_replace('-', '', strtolower($elements[0]));
362
                        if (sizeof($elements) > 2) {
363
                            $value = array_slice($elements, 1);
364
                        } else {
365
                            $value = isset($elements[1]) ? $elements[1] : null;
366
                        }
367
                        $scope = $this->getCurrentScope();
368
369
                        if ($name === 'include') {
370
                            if (!is_array($value)) {
371
                                $value = [$value];
372
                            }
373
                            foreach ($value as $path) {
374
                                if (substr($path, 0, 1) !== '/') {
375
                                    $path = 'conf/' . $path;
376
                                }
377
                                $files = glob($path);
378
                                if ($files) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $files of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
379
                                    foreach ($files as $fn) {
380
                                        try {
381
                                            static::parse($fn, $scope, true);
382
                                        } catch (InfiniteRecursion $e) {
383
                                            $this->raiseError('Cannot include \'' . $fn . '\' as a part of itself, it may cause an infinite recursion.');
384
                                        }
385
                                    }
386
                                }
387
                            }
388
                        } else {
389
                            if (sizeof($elements) === 1) {
390
                                $value = true;
391
                                $elements[1] = true;
392
                                $elTypes[1] = static::T_CVALUE;
393
                            } elseif ($value === null) {
394
                                $value = null;
395
                                $elements[1] = null;
396
                                $elTypes[1] = static::T_CVALUE;
397
                            }
398
399
                            if (isset($scope->{$name})) {
400
                                if ($scope->{$name}->source !== 'cmdline') {
401
                                    if (($elTypes[1] === static::T_CVALUE) && is_string($value)) {
402
                                        $scope->{$name}->pushHumanValue($value);
403
                                    } else {
404
                                        $scope->{$name}->pushValue($value);
405
                                    }
406
                                    $scope->{$name}->source = 'config';
407
                                    $scope->{$name}->revision = $this->revision;
408
                                }
409
                            } elseif ($scope instanceof Section) {
410
                                $scope->{$name} = new Generic();
411
                                $scope->{$name}->source = 'config';
412
                                $scope->{$name}->revision = $this->revision;
413
                                $scope->{$name}->pushValue($value);
414
                                $scope->{$name}->setValueType($value);
415
                            } else {
416
                                $this->raiseError('Unrecognized parameter \'' . $name . '\'');
417
                            }
418
                        }
419
                    } elseif ($tokenType === static::T_BLOCK) {
420
                        $scope = $this->getCurrentScope();
421
                        $sectionName = implode('-', $elements);
422
                        $sectionName = strtr($sectionName, '-. ', ':::');
423
                        if (!isset($scope->{$sectionName})) {
424
                            $scope->{$sectionName} = new Section;
425
                        }
426
                        $scope->{$sectionName}->source = 'config';
427
                        $scope->{$sectionName}->revision = $this->revision;
428
                        $this->state[] = [
429
                            static::T_ALL,
430
                            $scope->{$sectionName},
431
                        ];
432
                    }
433
                } else {
434
                    $this->raiseError('Unexpected char \'' . Debug::exportBytes($c) . '\'');
435
                }
436
            }
437
        ];
438
439
        for (; $this->p < $this->length; ++$this->p) {
440
            $c = $this->getCurrentChar();
441
            $e = end($this->state);
442
            $this->token($e[0], $c);
443
        }
444
        if (!$included) {
445
            $this->purgeScope($this->target);
446
        }
447
448
        if (Daemon::$config->verbosetty->value) {
449
            Daemon::log('Loaded config file: ' . escapeshellarg($file));
450
        }
451
    }
452
453
    /**
454
     * Removes old config parts after updating.
455
     * @return void
456
     */
457
    protected function purgeScope($scope)
458
    {
459
        foreach ($scope as $name => $obj) {
460
            if ($obj instanceof Generic) {
461
                if ($obj->source === 'config' && ($obj->revision < $this->revision)) {
462
                    if (!$obj->resetToDefault()) {
463
                        unset($scope->{$name});
464
                    }
465
                }
466
            } elseif ($obj instanceof Section) {
467
                if ($obj->source === 'config' && ($obj->revision < $this->revision)) {
468
                    if ($obj->count() === 0) {
469
                        unset($scope->{$name});
470
                    } elseif (isset($obj->enable)) {
471
                        $obj->enable->setValue(false);
472
                    }
473
                }
474
            }
475
        }
476
    }
477
478
    /**
479
     * Returns current variable scope
480
     * @return object Scope.
481
     */
482
    public function getCurrentScope()
483
    {
484
        $e = end($this->state);
485
486
        return $e[1];
487
    }
488
489
    /**
490
     * Raises error message.
491
     * @param string Message.
492
     * @param string Level.
493
     * @param string $msg
494
     * @return void
495
     */
496
    public function raiseError($msg, $level = 'emerg', $line = null, $col = null)
497
    {
498
        if ($level === 'emerg') {
499
            $this->erroneous = true;
500
        }
501
        if ($line === null) {
502
            $line = $this->line;
503
        }
504
        if ($col === null) {
505
            $col = $this->col - 1;
506
        }
507
508
        Daemon::log('[conf#' . $level . '][' . $this->file . ' L:' . $line . ' C: ' . $col . ']   ' . $msg);
509
    }
510
511
    /**
512
     * Executes token server.
513
     * @param string $c
514
     * @return mixed|void
515
     */
516
    protected function token($token, $c)
517
    {
518
        return $this->tokens[$token]($c);
519
    }
520
521
    /**
522
     * Current character.
523
     * @return string Character.
524
     */
525
    protected function getCurrentChar()
526
    {
527
        $c = substr($this->data, $this->p, 1);
528
529
        if ($c === "\n") {
530
            ++$this->line;
531
            $this->col = 1;
532
        } else {
533
            ++$this->col;
534
        }
535
536
        return $c;
537
    }
538
539
    /**
540
     * Returns next character.
541
     * @return string Character.
542
     */
543
    protected function getNextChar()
544
    {
545
        return substr($this->data, $this->p + 1, 1);
546
    }
547
548
    /**
549
     * Rewinds the pointer back.
550
     * @param integer Number of characters to rewind back.
551
     * @return void
552
     */
553
    protected function rewind($n)
554
    {
555
        $this->p -= $n;
556
    }
557
}
558