Completed
Push — master ( 602f00...8ac133 )
by stéphane
07:27
created

Node::onCompact()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 4
nop 1
dl 0
loc 17
rs 9.8666
c 0
b 0
f 0
1
<?php
2
0 ignored issues
show
Coding Style introduced by
Missing file doc comment
Loading history...
3
namespace Dallgoot\Yaml;
4
5
use Dallgoot\Yaml\{Yaml as Y, Regex as R};
6
7
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
8
 * 
9
 * @author  Stéphane Rebai <[email protected]>
10
 * @license Apache 2.0
11
 * @link    TODO : url to specific online doc
12
 */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @package tag in class comment
Loading history...
13
final class Node
14
{
15
    /** @var int */
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
16
    public $indent = -1;
17
    /** @var int */
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
18
    public $line;
19
    /** @var int */
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
20
    public $type;
21
    /** @var null|string|boolean */
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
22
    public $identifier;
23
    /** @var Node|NodeList|null|string */
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
24
    public $value = null;
25
26
    /** @var null|Node */
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
27
    private $parent;
0 ignored issues
show
Coding Style introduced by
Private member variable "parent" must be prefixed with an underscore
Loading history...
28
29
    /**
30
     * Create the Node object and parses $nodeString IF not null (else assume a root type Node)
31
     *
32
     * @param string|null $nodeString The node string
33
     * @param int|null    $line       The line
34
     */
35
    public function __construct($nodeString = null, $line = 0)
36
    {
37
        $this->line = (int) $line;
38
        if (is_null($nodeString)) {
39
            $this->type = Y::ROOT;
40
        } else {
41
            $this->parse($nodeString);
42
        }
43
    }
44
45
    /**
46
     * Sets the parent of the current Node
47
     * 
48
     * @param Node $node The node
49
     *
50
     * @return Node|self The currentNode
51
     */
52
    public function setParent(Node $node):Node
53
    {
54
        $this->parent = $node;
55
        return $this;
56
    }
57
58
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $type should have a doc-comment as per coding-style.
Loading history...
59
     * Gets the ancestor with specified $indent or the direct $parent OR the current Node itself
60
     *
61
     * @param int|null $indent The indent
62
     *
63
     * @return Node|self   The parent.
64
     */
65
    public function getParent(int $indent = null, $type = 0):Node
66
    {
67
        if ($this->type === Y::ROOT) {
68
            return $this;
69
        }
70
        if (!is_int($indent)) return $this->parent ?? $this;
71
        $cursor = $this;
72
        while ($cursor instanceof Node && $cursor->indent >= $indent) {
73
            if ($cursor->indent === $indent && $cursor->type !== $type) {
74
                $cursor = $cursor->parent;
75
                break;
76
            }
77
            $cursor = $cursor->parent;
78
        }
79
        return $cursor;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $cursor could return the type null which is incompatible with the type-hinted return Dallgoot\Yaml\Node. Consider adding an additional type-check to rule them out.
Loading history...
80
    }
81
82
    /**
83
     * Set the value for the current Node :
84
     * - if value is null , then value = $child (Node)
85
     * - if value is Node, then value is a NodeList with (previous value AND $child)
86
     * - if value is a NodeList, simply push $child into
87
     *
88
     * @param Node $child The child
89
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
90
    public function add(Node $child)
91
    {
92
        if ($this->type & (Y::SCALAR|Y::QUOTED)) {
93
            $this->getParent()->add($child);
94
            return;
95
        }
96
        $child->setParent($this);
97
        $current = $this->value;
98
        if (is_null($current)) {
99
            $this->value = $child;
100
        } else {
101
            if ($current instanceof Node) {
102
                $this->value = new NodeList();
103
                if ($current->type & Y::LITTERALS) {
104
                    $this->value->type = $current->type;
105
                } else {
106
                    $this->value->push($current);
107
                }
108
                //modify type according to child
109
                if ($current->type & Y::SET_KEY)   $this->value->type = Y::SET;
110
                if ($current->type & Y::KEY)       $this->value->type = Y::MAPPING;
111
                if ($current->type & Y::ITEM)      $this->value->type = Y::SEQUENCE;
112
            }
113
            $this->value->push($child);
0 ignored issues
show
Bug introduced by
The method push() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

113
            $this->value->/** @scrutinizer ignore-call */ 
114
                          push($child);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method push() does not exist on Dallgoot\Yaml\Node. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

113
            $this->value->/** @scrutinizer ignore-call */ 
114
                          push($child);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
114
115
            if ($this->type & Y::LITTERALS)  $this->value->type = $this->type;
116
        }
117
    }
118
119
    /**
120
     * Gets the deepest node.
121
     *
122
     * @return Node|self  The deepest node.
123
     */
124
    public function getDeepestNode():Node
125
    {
126
        $cursor = $this;
127
        while ($cursor->value instanceof Node || $cursor->value instanceof NodeList) {
128
            if ($cursor->value instanceof NodeList) {
129
                if ($cursor->value->count() === 1) {
130
                    $cursor = $cursor->value->OffsetGet(0);
131
                } else {
132
                    $cursor = $cursor;
133
                    break;
134
                }
135
            } else {
136
                $cursor = $cursor->value;
137
            }
138
        }
139
        return $cursor;
140
    }
141
142
    /**
143
     * Parses the string (assumed to be a line from a valid YAML)
144
     *
145
     * @param string $nodeString The node string
146
     *
147
     * @return Node|self
148
     */
149
    public function parse(string $nodeString):Node
150
    {
151
        $nodeValue = preg_replace("/^\t+/m", " ", $nodeString); //permissive to tabs but replacement
152
        $this->indent = strspn($nodeValue, ' ');
153
        $nodeValue = ltrim($nodeValue);
154
        if ($nodeValue === '') {
155
            $this->type = Y::BLANK;
156
        } elseif (substr($nodeValue, 0, 3) === '...') {//TODO: can have something on same line ?
157
            $this->type = Y::DOC_END;
158
        } elseif (preg_match(R::KEY, $nodeValue, $matches)) {
159
            $this->onKey($matches);
160
        } else {
161
            $this->identify($nodeValue);
162
        }
163
        return $this;
164
    }
165
166
    /**
167
     *  Set the type and value according to first character
168
     *
169
     * @param string $nodeValue The node value
170
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
171
    private function identify($nodeValue)
0 ignored issues
show
Coding Style introduced by
Private method name "Node::identify" must be prefixed with an underscore
Loading history...
172
    {
173
        $v = substr($nodeValue, 1);
174
        $first = $nodeValue[0];
175
        if (in_array($first, ['"', "'"])) {
176
            $this->type = R::isProperlyQuoted($nodeValue) ? Y::QUOTED : Y::PARTIAL;
177
            $this->value = $nodeValue;
178
            return;
179
        }
180
        if (in_array($first, ['{', '['])) {
181
             $this->onCompact($nodeValue);
182
             return;
183
         }
0 ignored issues
show
Coding Style introduced by
Closing brace indented incorrectly; expected 8 spaces, found 9
Loading history...
184
        if (in_array($first, ['!', '&', '*'])) {
185
            $this->onNodeAction($nodeValue);
186
            return;
187
        }
188
        // Note : php don't like '?' as an array key -_-'
189
        if(in_array($first, ['?', ':'])) {
0 ignored issues
show
Coding Style introduced by
Expected "if (...) {\n"; found "if(...) {\n"
Loading history...
190
            $this->type = $first === '?' ? Y::SET_KEY : Y::SET_VALUE;
191
            if (!empty(trim($v))) {
192
                $this->value = new NodeList;
193
                $this->add((new Node(ltrim($v), $this->line))->setParent($this));
194
            }
195
            return;
196
        }
197
        if ($first === "-") {
198
            $this->onHyphen($nodeValue);
199
            return;
200
        }
201
        $characters = [ '#' =>  [Y::COMMENT, ltrim($v)],
202
                        '%' =>  [Y::DIRECTIVE, ltrim($v)],
203
                        '>' =>  [Y::LITT_FOLDED, null],
204
                        '|' =>  [Y::LITT, null]
205
                        ];
206
        if (isset($characters[$first])) {
207
            $this->type  = $characters[$first][0];
208
            $this->value = $characters[$first][1];
209
        } else {
210
            $this->type  = Y::SCALAR;
211
            $this->value = $nodeValue;
212
        }
213
    }
214
215
    /**
216
     * Process when a "key: value" syntax is found in the parsed string
217
     * Note : key is match 1, value is match 2 as per regex from R::KEY
218
     *
219
     * @param array $matches The matches provided by 'preg_match' function in Node::parse
220
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
221
    private function onKey(array $matches)
0 ignored issues
show
Coding Style introduced by
Private method name "Node::onKey" must be prefixed with an underscore
Loading history...
222
    {
223
        $this->type = Y::KEY;
224
        $this->identifier = trim($matches[1], '"\' ');
225
        $value = $matches[2] ? trim($matches[2]) : null;
226
        if (!empty($value)) {
227
            $hasComment = strpos($value, ' #');
228
            if (!is_int($hasComment)) {
0 ignored issues
show
introduced by
The condition is_int($hasComment) is always true.
Loading history...
229
                $n = new Node($value, $this->line);
230
            } else {
231
                $n = new Node(trim(substr($value, 0, $hasComment)), $this->line);
232
                if ($tmpNode->type !== Y::PARTIAL) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $tmpNode seems to be never defined.
Loading history...
233
                    $comment = new Node(trim(substr($value, $hasComment + 1)), $this->line);
234
                    $comment->identifier = true; //to specify it is NOT a fullline comment
235
                    $this->add($comment->setParent($this));
236
                }
237
            }
238
            $n->indent = $this->indent + strlen($this->identifier);
239
            $this->add($n->setParent($this));
240
        }
241
    }
242
243
    /**
244
     * Determines the correct type and value when a short object/array syntax is found
245
     *
246
     * @param string $value The value assumed to start with { or ( or characters
247
     * 
248
     * @see Node::identify
249
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
250
    private function onCompact($value)
0 ignored issues
show
Coding Style introduced by
Private method name "Node::onCompact" must be prefixed with an underscore
Loading history...
251
    {
252
        $this->value = $value;
253
        json_decode($value, false, 512, JSON_PARTIAL_OUTPUT_ON_ERROR|JSON_UNESCAPED_SLASHES);
254
        if (json_last_error() === JSON_ERROR_NONE){
0 ignored issues
show
Coding Style introduced by
Expected "if (...) {\n"; found "if (...){\n"
Loading history...
Coding Style introduced by
There must be a single space between the closing parenthesis and the opening brace of a multi-line IF statement; found 0 spaces
Loading history...
255
            $this->type = Y::JSON;
256
            return;
257
        }
258
        if (preg_match(R::MAPPING, $value)){
0 ignored issues
show
Coding Style introduced by
Expected "if (...) {\n"; found "if (...){\n"
Loading history...
Coding Style introduced by
There must be a single space between the closing parenthesis and the opening brace of a multi-line IF statement; found 0 spaces
Loading history...
259
            $this->type = Y::COMPACT_MAPPING;
260
            return;
261
        }
262
        if (preg_match(R::SEQUENCE, $value)){
0 ignored issues
show
Coding Style introduced by
Expected "if (...) {\n"; found "if (...){\n"
Loading history...
Coding Style introduced by
There must be a single space between the closing parenthesis and the opening brace of a multi-line IF statement; found 0 spaces
Loading history...
263
            $this->type = Y::COMPACT_SEQUENCE;
264
            return;
265
        }
266
        $this->type = Y::PARTIAL;
267
    }
268
269
    /**
270
     * Determines type and value when an hyphen "-" is found
271
     *
272
     * @param string $nodeValue The node value
273
     * 
274
     * @see Node::identify
275
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
276
    private function onHyphen($nodeValue)
0 ignored issues
show
Coding Style introduced by
Private method name "Node::onHyphen" must be prefixed with an underscore
Loading history...
277
    {
278
        if (substr($nodeValue, 0, 3) === '---') {
279
            $this->type = Y::DOC_START;
280
            $rest = trim(substr($nodeValue, 3));
281
            if (empty($rest)) {
282
                return;
283
            }
284
            $n = new Node($rest, $this->line);
285
            $n->indent = $this->indent + 4;
286
            $this->value = $n->setParent($this);
287
            return;
288
        }
289
        if (preg_match(R::ITEM, $nodeValue, $matches)) {
290
            $this->type = Y::ITEM;
291
            if (isset($matches[1]) && !empty(trim($matches[1]))) {
292
                $n = new Node(trim($matches[1]), $this->line);
293
                $n->indent = $this->indent + 2;
294
                $this->value = $n->setParent($this);
295
            }
296
            return;
297
        }
298
        list($this->type, $this->value) = [Y::SCALAR, $nodeValue];
299
    }
300
301
    /**
302
     * Determines the type and value according to $nodeValue when one of these characters is found : !,&,*
303
     *
304
     * @param string $nodeValue The node value
305
     * 
306
     * @see  Node::identify
307
     * @todo handle tags like  <tag:clarkevans.com,2002:invoice>
308
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
309
    private function onNodeAction($nodeValue)
0 ignored issues
show
Coding Style introduced by
Private method name "Node::onNodeAction" must be prefixed with an underscore
Loading history...
310
    {
311
        $v = substr($nodeValue, 1);
312
        $this->type = ['!' => Y::TAG, '&' => Y::REF_DEF, '*' => Y::REF_CALL][$nodeValue[0]];
313
        $this->identifier = $v;
314
        $pos = strpos($v, ' ');
315
        $this->value = new NodeList;
316
        if (is_int($pos)) {
0 ignored issues
show
introduced by
The condition is_int($pos) is always true.
Loading history...
317
            $this->identifier = strstr($v, ' ', true);
318
            $value = trim(substr($nodeValue, $pos + 1));
319
            $value = R::isProperlyQuoted($value) ? trim($value, "\"'") : $value;
320
            $this->add((new Node($value, $this->line))->setParent($this));
321
        }
322
    }
323
324
    /**
325
     * PHP internal function for debugging purpose : simplify output provided by 'var_dump'
326
     *
327
     * @return array  the Node properties and respective values displayed by 'var_dump'
328
     */
329
    public function __debugInfo():array
330
    {
331
        return ['line'  => $this->line,
332
                'indent'=> $this->indent,
333
                'type'  => Y::getName($this->type).($this->identifier ? "($this->identifier)" : ''),
334
                'value' => $this->value];
335
    }
336
}
337