Completed
Pull Request — master (#234)
by Дмитрий
07:51
created

Parser::purgeScope()   B

Complexity

Conditions 11
Paths 9

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 11
dl 0
loc 20
rs 7.1162
eloc 12
c 1
b 1
f 0
nc 9
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace PHPDaemon\Config;
3
4
use PHPDaemon\Config\Entry\Generic;
5
use PHPDaemon\Config\Object;
6
use PHPDaemon\Config\Section;
7
use PHPDaemon\Core\Daemon;
8
use PHPDaemon\Core\Debug;
9
use PHPDaemon\Exceptions\InfiniteRecursion;
10
11
/**
12
 * Config parser
13
 *
14
 * @package    Core
15
 * @subpackage Config
16
 *
17
 * @author     Vasily Zorin <[email protected]>
18
 */
19
class Parser
20
{
21
    use \PHPDaemon\Traits\ClassWatchdog;
22
    use \PHPDaemon\Traits\StaticObjectWatchdog;
23
24
    /**
25
     * State: standby
26
     */
27
    const T_ALL     = 1;
28
    /**
29
     * State: comment
30
     */
31
    const T_COMMENT = 2;
32
    /**
33
     * State: variable definition block
34
     */
35
    const T_VAR     = 3;
36
    /**
37
     * Single-quoted string
38
     */
39
    const T_STRING  = 4;
40
    
41
    /**
42
     * Double-quoted
43
     */
44
    const T_STRING_DOUBLE = 5;
45
    
46
    /**
47
     * Block
48
     */
49
    const T_BLOCK   = 6;
50
    
51
    /**
52
     * Value defined by constant (keyword) or number
53
     */
54
    const T_CVALUE  = 7;
55
56
    /**
57
     * Config file path
58
     * @var string
59
     */
60
    protected $file;
61
62
    /**
63
     * Current line number
64
     * @var number
65
     */
66
    protected $line = 1;
67
68
    /**
69
     * Current column number
70
     * @var number
71
     */
72
    protected $col = 1;
73
74
    /**
75
     * Pointer (current offset)
76
     * @var integer
77
     */
78
    protected $p = 0;
79
80
    /**
81
     * State stack
82
     * @var array
83
     */
84
    protected $state = [];
85
86
    /**
87
     * Target object
88
     * @var object
89
     */
90
    protected $target;
91
92
    /**
93
     * Erroneous?
94
     * @var boolean
95
     */
96
    protected $erroneous = false;
97
98
    /**
99
     * Callbacks
100
     * @var array
101
     */
102
    protected $tokens;
103
104
    /**
105
     * File length
106
     * @var integer
107
     */
108
    protected $length;
109
110
    /**
111
     * Revision
112
     * @var integer
113
     */
114
    protected $revision;
115
116
    /**
117
     * Contents of config file
118
     * @var string
119
     */
120
    protected $data;
121
122
    /**
123
     * Parse stack
124
     * @var array
125
     */
126
    protected static $stack = [];
127
128
    /**
129
     * Erroneous?
130
     * @return boolean
131
     */
132
    public function isErroneous()
133
    {
134
        return $this->erroneous;
135
    }
136
137
    /**
138
     * Parse config file
139
     * @param string  File path
140
     * @param object  Target
141
     * @param boolean Included? Default is false
142
     * @return \PHPDaemon\Config\Parser
143
     */
144
    public static function parse($file, $target, $included = false)
145
    {
146
        if (in_array($file, static::$stack)) {
147
            throw new InfiniteRecursion;
148
        }
149
150
        static::$stack[] = $file;
151
        $parser = new static($file, $target, $included);
152
        array_pop(static::$stack);
153
        return $parser;
154
    }
155
156
    /**
157
     * Constructor
158
     * @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...
159
     */
160
    protected function __construct($file, $target, $included = false)
161
    {
162
        $this->file     = $file;
163
        $this->target   = $target;
164
        $this->revision = ++Object::$lastRevision;
165
        $this->data     = file_get_contents($file);
166
167
        if (substr($this->data, 0, 2) === '#!') {
168
            if (!is_executable($file)) {
169
                $this->raiseError('Shebang (#!) detected in the first line, but file hasn\'t +x mode.');
170
                return;
171
            }
172
            $this->data = shell_exec($file);
173
        }
174
175
        $this->data    = str_replace("\r", '', $this->data);
176
        $this->length     = mb_orig_strlen($this->data);
177
        $this->state[] = [static::T_ALL, $this->target];
178
        $this->tokens  = [
179
            static::T_COMMENT => function ($c) {
180
                if ($c === "\n") {
181
                    array_pop($this->state);
182
                }
183
            },
184
            static::T_STRING_DOUBLE  => function ($q) {
185
                $str = '';
186
                ++$this->p;
187
188
                for (; $this->p < $this->length; ++$this->p) {
0 ignored issues
show
Comprehensibility Bug introduced by
Loop incrementor ($this) jumbling with inner loop
Loading history...
189
                    $c = $this->getCurrentChar();
190
191
                    if ($c === $q) {
192
                        ++$this->p;
193
                        break;
194
                    } elseif ($c === '\\') {
195
                        next:
196
                        $n = $this->getNextChar();
197
                        if ($n === $q) {
198
                            $str .= $q;
199
                            ++$this->p;
200
                        } elseif (ctype_digit($n)) {
201
                            $def = $n;
202
                            ++$this->p;
203 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...
204
                                $n = $this->getNextChar();
205
                                if (!ctype_digit($n)) {
206
                                    break;
207
                                }
208
                                $def .= $n;
209
                            }
210
                            $str .= chr((int) $def);
211
                        } elseif (($n === 'x') || ($n === 'X')) {
212
                            $def = $n;
213
                            ++$this->p;
214 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...
215
                                $n = $this->getNextChar();
216
                                if (!ctype_xdigit($n)) {
217
                                    break;
218
                                }
219
                                $def .= $n;
220
                            }
221
                            $str .= chr((int) hexdec($def));
222
                        } else {
223
                            $str .= $c;
224
                        }
225
                    } else {
226
                        $str .= $c;
227
                    }
228
                }
229
230
                if ($this->p >= $this->length) {
231
                    $this->raiseError('Unexpected End-Of-File.');
232
                }
233
                return $str;
234
            },
235
            static::T_STRING => function ($q) {
236
                $str = '';
237
                ++$this->p;
238
239
                for (; $this->p < $this->length; ++$this->p) {
240
                    $c = $this->getCurrentChar();
241
242
                    if ($c === $q) {
243
                        ++$this->p;
244
                        break;
245
                    } elseif ($c === '\\') {
246
                        if ($this->getNextChar() === $q) {
247
                            $str .= $q;
248
                            ++$this->p;
249
                        } else {
250
                            $str .= $c;
251
                        }
252
                    } else {
253
                        $str .= $c;
254
                    }
255
                }
256
257
                if ($this->p >= $this->length) {
258
                    $this->raiseError('Unexpected End-Of-File.');
259
                }
260
                return $str;
261
            },
262
            static::T_ALL     => function ($c) {
263
                if (ctype_space($c)) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
264
                } elseif ($c === '#') {
265
                    $this->state[] = [static::T_COMMENT];
266
                } elseif ($c === '}') {
267
                    if (sizeof($this->state) > 1) {
268
                        $this->purgeScope($this->getCurrentScope());
269
                        array_pop($this->state);
270
                    } else {
271
                        $this->raiseError('Unexpected \'}\'');
272
                    }
273
                } elseif (ctype_alnum($c) || $c === '\\') {
274
                    $elements        = [''];
275
                    $elTypes         = [null];
276
                    $i               = 0;
277
                    $tokenType       = 0;
278
                    $newLineDetected = null;
279
280
                    for (; $this->p < $this->length; ++$this->p) {
281
                        $prePoint = [$this->line, $this->col - 1];
282
                        $c        = $this->getCurrentChar();
283
284
                        if (ctype_space($c) || $c === '=' || $c === ',') {
285
                            if ($c === "\n") {
286
                                $newLineDetected = $prePoint;
287
                            }
288
                            if ($elTypes[$i] !== null) {
289
                                ++$i;
290
                                $elTypes[$i] = null;
291
                            }
292
                        } elseif ($c === '\'') {
293
                            if ($elTypes[$i] !== null) {
294
                                $this->raiseError('Unexpected T_STRING.');
295
                            }
296
297
                            $string = $this->token(static::T_STRING, $c);
298
                            --$this->p;
299
300
                            if ($elTypes[$i] === null) {
301
                                $elements[$i] = $string;
302
                                $elTypes[$i]  = static::T_STRING;
303
                            }
304
                        } elseif ($c === '"') {
305
                            if ($elTypes[$i] !== null) {
306
                                $this->raiseError('Unexpected T_STRING_DOUBLE.');
307
                            }
308
309
                            $string = $this->token(static::T_STRING_DOUBLE, $c);
310
                            --$this->p;
311
312
                            if ($elTypes[$i] === null) {
313
                                $elements[$i] = $string;
314
                                $elTypes[$i]  = static::T_STRING_DOUBLE;
315
                            }
316
                        } elseif ($c === '}') {
317
                            $this->raiseError('Unexpected \'}\' instead of \';\' or \'{\'');
318
                        } elseif ($c === ';') {
319
                            if ($newLineDetected) {
320
                                $this->raiseError('Unexpected new-line instead of \';\'', 'notice', $newLineDetected[0], $newLineDetected[1]);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 142 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...
321
                            }
322
                            $tokenType = static::T_VAR;
323
                            break;
324
                        } elseif ($c === '{') {
325
                            $tokenType = static::T_BLOCK;
326
                            break;
327
                        } else {
328
                            if ($elTypes[$i] === static::T_STRING) {
329
                                $this->raiseError('Unexpected T_CVALUE.');
330
                            } else {
331
                                if (!isset($elements[$i])) {
332
                                    $elements[$i] = '';
333
                                }
334
335
                                $elements[$i] .= $c;
336
                                $elTypes[$i] = static::T_CVALUE;
337
                            }
338
                        }
339
                    }
340
                    foreach ($elTypes as $k => $v) {
341
                        if (static::T_CVALUE === $v) {
342
                            if (ctype_digit($elements[$k])) {
343
                                $elements[$k] = (int)$elements[$k];
344
                            } elseif (is_numeric($elements[$k])) {
345
                                $elements[$k] = (float)$elements[$k];
346
                            } else {
347
                                $l = strtolower($elements[$k]);
348
349
                                if (($l === 'true') || ($l === 'on')) {
350
                                    $elements[$k] = true;
351
                                } elseif (($l === 'false') || ($l === 'off')) {
352
                                    $elements[$k] = false;
353
                                } elseif ($l === 'null') {
354
                                    $elements[$k] = null;
355
                                }
356
                            }
357
                        }
358
                    }
359
                    if ($tokenType === 0) {
360
                        $this->raiseError('Expected \';\' or \'{\'');
361
                    } elseif ($tokenType === static::T_VAR) {
362
                        $name = str_replace('-', '', strtolower($elements[0]));
363
                        if (sizeof($elements) > 2) {
364
                            $value = array_slice($elements, 1);
365
                        } else {
366
                            $value = isset($elements[1]) ? $elements[1] : null;
367
                        }
368
                        $scope = $this->getCurrentScope();
369
370
                        if ($name === 'include') {
371
                            if (!is_array($value)) {
372
                                $value = [$value];
373
                            }
374
                            foreach ($value as $path) {
375
                                if (substr($path, 0, 1) !== '/') {
376
                                    $path = 'conf/' . $path;
377
                                }
378
                                $files = glob($path);
379
                                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...
380
                                    foreach ($files as $fn) {
381
                                        try {
382
                                            static::parse($fn, $scope, true);
383
                                        } catch (InfiniteRecursion $e) {
384
                                            $this->raiseError('Cannot include \'' . $fn . '\' as a part of itself, it may cause an infinite recursion.');
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 153 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...
385
                                        }
386
                                    }
387
                                }
388
                            }
389
                        } else {
390
                            if (sizeof($elements) === 1) {
391
                                $value       = true;
392
                                $elements[1] = true;
393
                                $elTypes[1]  = static::T_CVALUE;
394
                            } elseif ($value === null) {
395
                                $value       = null;
396
                                $elements[1] = null;
397
                                $elTypes[1]  = static::T_CVALUE;
398
                            }
399
400
                            if (isset($scope->{$name})) {
401
                                if ($scope->{$name}->source !== 'cmdline') {
402
                                    if (($elTypes[1] === static::T_CVALUE) && is_string($value)) {
403
                                        $scope->{$name}->pushHumanValue($value);
404
                                    } else {
405
                                        $scope->{$name}->pushValue($value);
406
                                    }
407
                                    $scope->{$name}->source   = 'config';
408
                                    $scope->{$name}->revision = $this->revision;
409
                                }
410
                            } elseif ($scope instanceof Section) {
411
                                $scope->{$name}           = new Generic();
412
                                $scope->{$name}->source   = 'config';
413
                                $scope->{$name}->revision = $this->revision;
414
                                $scope->{$name}->pushValue($value);
415
                                $scope->{$name}->setValueType($value);
416
                            } else {
417
                                $this->raiseError('Unrecognized parameter \'' . $name . '\'');
418
                            }
419
                        }
420
                    } elseif ($tokenType === static::T_BLOCK) {
421
                        $scope       = $this->getCurrentScope();
422
                        $sectionName = implode('-', $elements);
423
                        $sectionName = strtr($sectionName, '-. ', ':::');
424
                        if (!isset($scope->{$sectionName})) {
425
                            $scope->{$sectionName} = new Section;
426
                        }
427
                        $scope->{$sectionName}->source   = 'config';
428
                        $scope->{$sectionName}->revision = $this->revision;
429
                        $this->state[]                   = [
430
                            static::T_ALL,
431
                            $scope->{$sectionName},
432
                        ];
433
                    }
434
                } else {
435
                    $this->raiseError('Unexpected char \'' . Debug::exportBytes($c) . '\'');
436
                }
437
            }
438
        ];
439
440
        for (; $this->p < $this->length; ++$this->p) {
441
            $c = $this->getCurrentChar();
442
            $e = end($this->state);
443
            $this->token($e[0], $c);
444
        }
445
        if (!$included) {
446
            $this->purgeScope($this->target);
447
        }
448
        
449
        if (Daemon::$config->verbosetty->value) {
450
            Daemon::log('Loaded config file: '. escapeshellarg($file));
451
        }
452
    }
453
454
    /**
455
     * Removes old config parts after updating.
456
     * @return void
457
     */
458
    protected function purgeScope($scope)
459
    {
460
        foreach ($scope as $name => $obj) {
461
            if ($obj instanceof Generic) {
462
                if ($obj->source === 'config' && ($obj->revision < $this->revision)) {
463
                    if (!$obj->resetToDefault()) {
464
                        unset($scope->{$name});
465
                    }
466
                }
467
            } elseif ($obj instanceof Section) {
468
                if ($obj->source === 'config' && ($obj->revision < $this->revision)) {
469
                    if ($obj->count() === 0) {
470
                        unset($scope->{$name});
471
                    } elseif (isset($obj->enable)) {
472
                        $obj->enable->setValue(false);
473
                    }
474
                }
475
            }
476
        }
477
    }
478
479
    /**
480
     * Returns current variable scope
481
     * @return object Scope.
482
     */
483
    public function getCurrentScope()
484
    {
485
        $e = end($this->state);
486
487
        return $e[1];
488
    }
489
490
    /**
491
     * Raises error message.
492
     * @param string Message.
493
     * @param string Level.
494
     * @param string $msg
495
     * @return void
496
     */
497
    public function raiseError($msg, $level = 'emerg', $line = null, $col = null)
498
    {
499
        if ($level === 'emerg') {
500
            $this->erroneous = true;
501
        }
502
        if ($line === null) {
503
            $line = $this->line;
504
        }
505
        if ($col === null) {
506
            $col = $this->col - 1;
507
        }
508
509
        Daemon::log('[conf#' . $level . '][' . $this->file . ' L:' . $line . ' C: ' . $col . ']   ' . $msg);
510
    }
511
512
    /**
513
     * Executes token server.
514
     * @param string $c
515
     * @return mixed|void
516
     */
517
    protected function token($token, $c)
518
    {
519
        return $this->tokens[$token]($c);
520
    }
521
522
    /**
523
     * Current character.
524
     * @return string Character.
525
     */
526
    protected function getCurrentChar()
527
    {
528
        $c = substr($this->data, $this->p, 1);
529
530
        if ($c === "\n") {
531
            ++$this->line;
532
            $this->col = 1;
533
        } else {
534
            ++$this->col;
535
        }
536
537
        return $c;
538
    }
539
540
    /**
541
     * Returns next character.
542
     * @return string Character.
543
     */
544
    protected function getNextChar()
545
    {
546
        return substr($this->data, $this->p + 1, 1);
547
    }
548
549
    /**
550
     * Rewinds the pointer back.
551
     * @param integer Number of characters to rewind back.
552
     * @return void
553
     */
554
    protected function rewind($n)
555
    {
556
        $this->p -= $n;
557
    }
558
}
559