Completed
Push — master ( b9ada3...d95e42 )
by stéphane
01:58
created

Builder::buildNode()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 22
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 19
nc 6
nop 2
dl 0
loc 22
rs 9.0111
c 0
b 0
f 0
1
<?php
2
3
namespace Dallgoot\Yaml;
4
5
use Dallgoot\Yaml\{Yaml as Y, Regex as R};
6
7
/**
8
 * Constructs the result (YamlObject or array) according to every Node and respecting value
9
 *
10
 * @author  Stéphane Rebai <[email protected]>
11
 * @license Apache 2.0
12
 * @link    TODO : url to specific online doc
13
 */
14
final class Builder
15
{
16
    private static $_root;
17
    private static $_debug;
18
19
    const ERROR_NO_KEYNAME = self::class.": key has NO IDENTIFIER on line %d";
20
    const INVALID_DOCUMENT = self::class.": DOCUMENT %d can NOT be a mapping AND a sequence";
21
22
    /**
23
     * Builds a file.  check multiple documents & split if more than one documents
24
     *
25
     * @param   Node   $_root      The root node : Node with Node->type === YAML::ROOT
26
     * @param   int   $_debug      the level of debugging requested
27
     *
28
     * @return array|YamlObject      list of documents or just one.
29
     * @todo  implement splitting on YAML::DOC_END also
30
     */
31
    public static function buildContent(Node $_root, int $_debug)
32
    {
33
        self::$_debug = $_debug;
34
        $totalDocStart = 0;
35
        $documents = [];
36
        $_root->value instanceof NodeList && $_root->value->setIteratorMode(NodeList::IT_MODE_DELETE);
37
        foreach ($_root->value as $child) {
38
            if ($child->type & Y::DOC_START) $totalDocStart++;
39
            //if 0 or 1 DOC_START = we are still in first document
40
            $currentDoc = $totalDocStart > 1 ? $totalDocStart - 1 : 0;
41
            if (!isset($documents[$currentDoc])) $documents[$currentDoc] = new NodeList();
42
            $documents[$currentDoc]->push($child);
43
        }
44
        $content = [];
45
        foreach ($documents as $docNum => $list) {
46
            self::$_root = new YamlObject;
47
            try {
48
                $content[] = self::buildNodeList($list, self::$_root);
49
            } catch (\Exception $e) {
50
                throw new \ParseError(sprintf(self::INVALID_DOCUMENT, $docNum));
51
            }
52
        }
53
        return count($content) === 1 ? $content[0] : $content;
54
    }
55
56
    /**
57
     * Generic function to distinguish between Node and NodeList
58
     *
59
     * @param Node|NodeList $node   The node.
60
     * @param mixed         $parent The parent
61
     *
62
     * @return mixed  ( description_of_the_return_value )
63
     */
64
    private static function build(object $node, &$parent = null)
65
    {
66
        if ($node instanceof NodeList) return self::buildNodeList($node, $parent);
67
        return self::buildNode($node, $parent);
68
    }
69
70
    /**
71
     * Builds a node list.
72
     *
73
     * @param NodeList $node   The node
74
     * @param mixed    $parent The parent
75
     *
76
     * @return mixed    The parent (object|array) or a string representing the NodeList.
77
     */
78
    private static function buildNodeList(NodeList $node, &$parent=null)
79
    {
80
        $node->forceType();
81
        if ($node->type & (Y::RAW | Y::LITTERALS)) {
82
            return self::buildLitteral($node, (int) $node->type);
83
        }
84
        $action = function ($child, &$parent, &$out) {
85
            self::build($child, $out);
86
        };
87
        if ($node->type & (Y::COMPACT_MAPPING|Y::MAPPING|Y::SET)) {
88
            $out = $parent ?? new \StdClass;
89
        } elseif ($node->type & (Y::COMPACT_SEQUENCE|Y::SEQUENCE)) {
90
            $out = $parent ?? [];
91
        } else {
92
            $out = '';
93
            $action = function ($child, &$parent, &$out) {
94
                if ($child->type & (Y::SCALAR|Y::QUOTED)) {
95
                    if ($parent) {
96
                        $parent->setText(self::build($child));
97
                    } else {
98
                        $out .= self::build($child);
99
                    }
100
                }
101
            };
102
        }
103
        foreach ($node as $child) {
104
            $action($child, $parent, $out);
105
        }
106
        if ($node->type & (Y::COMPACT_SEQUENCE|Y::COMPACT_MAPPING)) {
107
            $out = new Compact($out);
0 ignored issues
show
Bug introduced by
It seems like $out can also be of type string; however, parameter $candidate of Dallgoot\Yaml\Compact::__construct() does only seem to accept array|object, 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

107
            $out = new Compact(/** @scrutinizer ignore-type */ $out);
Loading history...
108
        }
109
        return is_null($out) ? $parent : $out;
110
    }
111
112
    /**
113
     * Builds a node.
114
     *
115
     * @param Node    $node    The node of any Node->type
116
     * @param mixed  $parent  The parent
117
     *
118
     * @return mixed  The node value as Scalar, Array, Object or Null otherwise.
119
     */
120
    private static function buildNode(Node $node, &$parent)
121
    {
122
        extract((array) $node, EXTR_REFS);
123
        $actions = [Y::DIRECTIVE => 'buildDirective',
124
                    Y::ITEM      => 'buildItem',
125
                    Y::KEY       => 'buildKey',
126
                    Y::SET_KEY   => 'buildSetKey',
127
                    Y::SET_VALUE => 'buildSetValue',
128
                    Y::TAG       => 'buildTag',
129
        ];
130
        if (isset($actions[$type])) {
131
            return self::{$actions[$type]}($node, $parent);
132
        } elseif ($type & Y::COMMENT) {
133
            self::$_root->addComment($line, $value);
134
        } elseif ($type & (Y::COMPACT_MAPPING|Y::COMPACT_SEQUENCE)) {
135
            return self::buildNodeList($value, $parent);
136
        } elseif ($type & (Y::REF_DEF | Y::REF_CALL)) {
137
            return self::handleReference($node, $parent);
138
        } elseif ($value instanceof Node) {
139
            return self::buildNode($value, $parent);
140
        } else {
141
            return Node2PHP::get($node);
142
        }
143
    }
144
145
    private static function handleReference($node, $parent)
146
    {
147
        $tmp = is_null($node->value) ? null : self::build($node->value, $parent);
148
        if ($node->type === Y::REF_DEF) self::$_root->addReference($node->identifier, $tmp);
149
        return self::$_root->getReference($node->identifier);
150
    }
151
152
153
    /**
154
     * Builds a key and set the property + value to the given parent
155
     *
156
     * @param Node $node       The node with type YAML::KEY
157
     * @param object|array $parent       The parent
158
     *
159
     * @throws \ParseError if Key has no name(identifier) Note: empty string is allowed
160
     * @return null
161
     */
162
    private static function buildKey(Node $node, &$parent=null)
163
    {
164
        extract((array) $node, EXTR_REFS);
165
        if (is_null($identifier)) {
166
            throw new \ParseError(sprintf(self::ERROR_NO_KEYNAME, $line));
167
        } else {
168
            if ($value instanceof Node) {
169
                if ($value->type & (Y::ITEM|Y::KEY)) {
170
                    $value = new NodeList($value);
171
                } else {
172
                    $result = self::build($value);
173
                }
174
            }
175
            if ($value instanceof NodeList) {
176
                $result = self::buildNodeList($value);
177
            }
178
            if (is_null($parent)) {
179
                return $result;
180
            } else {
181
                if (is_array($parent)) {
182
                    $parent[$identifier] = $result;
183
                } else {
184
                    $parent->{$identifier} = $result;
185
                }
186
            }
187
        }
188
    }
189
190
    /**
191
     * Builds an item. Adds the item value to the parent array|Iterator
192
     *
193
     * @param      Node        $node    The node with type YAML::ITEM
194
     * @param      array|\Iterator      $parent  The parent
195
     *
196
     * @throws     \Exception  if parent is another type than array or object Iterator
197
     * @return null
198
     */
199
    private static function buildItem(Node $node, &$parent)
200
    {
201
        extract((array) $node, EXTR_REFS);
202
        if (!is_array($parent) && !($parent instanceof \ArrayIterator)) {
203
            throw new \Exception("parent must be an Iterable not ".(is_object($parent) ? get_class($parent) : gettype($parent)), 1);
204
        }
205
        $ref = $parent instanceof \ArrayIterator ? $parent->getArrayCopy() : $parent;
0 ignored issues
show
introduced by
$parent is never a sub-type of ArrayIterator.
Loading history...
206
        $numKeys = array_filter(array_keys($ref), 'is_int');
207
        $key = count($numKeys) > 0 ? max($numKeys) + 1 : 0;
208
        if ($value instanceof Node) {
209
            if($value->type & Y::KEY) {
210
                self::buildKey($node->value, $parent);
211
                return;
212
            } elseif ($value->type & Y::ITEM) {
213
                $a = [];
214
                $result = self::buildItem($value, $a);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $result is correct as self::buildItem($value, $a) targeting Dallgoot\Yaml\Builder::buildItem() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
215
            }
216
        }
217
        $result = self::build($value);
218
        $parent[$key] = $result;
219
    }
220
221
222
    /**
223
     * Builds a litteral (folded or not) or any NodeList that has YAML::RAW type (like a multiline value)
224
     *
225
     * @param      NodeList  $children  The children
226
     * @param      integer   $type      The type
227
     *
228
     * @return     string    The litteral.
229
     * @todo : Example 6.1. Indentation Spaces  spaces must be considered as content
230
     */
231
    private static function buildLitteral(NodeList $list, int $type):string
232
    {
233
        $list->rewind();
234
        $refIndent = $list->current()->indent;
235
        //remove trailing blank
236
        while ($list->top()->type & Y::BLANK) $list->pop();
237
        $result = '';
238
        $separator = [ Y::RAW => '', Y::LITT => "\n", Y::LITT_FOLDED => ' '][$type];
239
        foreach ($list as $child) {
240
            if ($child->value instanceof NodeList) {
241
                $result .= self::buildLitteral($child->value, $type).$separator;
242
            } else {
243
                $val = $child->type & (Y::SCALAR|Y::BLANK) ? $child->value : substr($child->raw, $refIndent);
244
                if ($type & Y::LITT_FOLDED && ($child->indent > $refIndent || ($child->type & Y::BLANK))) {
245
                    if ($result[-1] === $separator)
246
                        $result[-1] = "\n";
247
                    if ($result[-1] === "\n")
248
                        $result .= $val;
249
                    continue;
250
                }
251
                $result .= $val.$separator;
252
            }
253
        }
254
        return rtrim($result);
255
    }
256
257
    /**
258
     * Builds a set key.
259
     *
260
     * @param      Node        $node    The node of type YAML::SET_KEY.
261
     * @param      object      $parent  The parent
262
     *
263
     * @throws     \Exception  if a problem occurs during serialisation (json format) of the key
264
     */
265
    private function buildSetKey(Node $node, &$parent)
266
    {
267
        $built = is_object($node->value) ? self::build($node->value) : null;
268
        $stringKey = is_string($built) && Regex::isProperlyQuoted($built) ? trim($built, '\'" '): $built;
269
        $key = json_encode($stringKey, JSON_PARTIAL_OUTPUT_ON_ERROR|JSON_UNESCAPED_SLASHES);
270
        if (empty($key)) throw new \Exception("Cant serialize complex key: ".var_export($node->value, true), 1);
271
        $parent->{trim($key, '\'" ')} = null;
272
    }
273
274
    /**
275
     * Builds a set value.
276
     *
277
     * @param      Node    $node    The node of type YAML::SET_VALUE
278
     * @param      object  $parent  The parent (the document object or any previous object created through a mapping key)
279
     */
280
    private function buildSetValue(Node $node, &$parent)
281
    {
282
        $prop = array_keys(get_object_vars($parent));
283
        $key = end($prop);
284
        if ($node->value->type & (Y::ITEM|Y::KEY )) {
285
            $node->value = new NodeList($node->value);
286
        }
287
        $parent->{$key} = self::build($node->value);
0 ignored issues
show
Bug introduced by
It seems like $node->value can also be of type null and string; however, parameter $node of Dallgoot\Yaml\Builder::build() does only seem to accept object, 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

287
        $parent->{$key} = self::build(/** @scrutinizer ignore-type */ $node->value);
Loading history...
288
    }
289
290
    /**
291
     * Builds a tag and its value (also built) and encapsulates them in a Tag object.
292
     *
293
     * @param      Node    $node    The node of type YAML::TAG
294
     * @param      mixed  $parent  The parent
295
     *
296
     * @return     Tag|null     The tag object of class Dallgoot\Yaml\Tag.
297
     */
298
    private static function buildTag(Node $node, &$parent)
299
    {
300
        $name = (string) $node->identifier;
301
        if ($parent === self::$_root && empty($node->value)) {
302
            $parent->addTag($name);
303
        } else {
304
            $target = $node->value;
305
            if ($node->value instanceof Node) {
306
                if ($node->value->type & (Y::KEY|Y::ITEM)) {
307
                    if (is_null($parent)) {
308
                        $target = new NodeList($node->value);
309
                    } else {
310
                        self::build($node->value, $parent);
311
                    }
312
                }
313
            }
314
            //TODO: have somewhere a list of common tags and their treatment
315
            // if (in_array($node->identifier, ['!binary', '!str'])) {
316
            //     $target->type = Y::RAW;
317
            // }
318
            return new Tag($name, is_object($target) ? self::build($target) : null);
319
        }
320
    }
321
322
    /**
323
     * Builds a directive. NOT IMPLEMENTED YET
324
     *
325
     * @param      Node  $node    The node
326
     * @param      mixed  $parent  The parent
327
     * @todo implement if requested
328
     */
329
    private function buildDirective(Node $node, $parent)
330
    {
331
        // TODO : implement
332
    }
333
}
334