Completed
Push — master ( d95e42...809c98 )
by stéphane
02:23
created

Builder::buildLitteral()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 6
nop 2
dl 0
loc 16
rs 9.9
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
        $buffer = new NodeList();
37
        $_root->value->setIteratorMode(NodeList::IT_MODE_DELETE);
0 ignored issues
show
Bug introduced by
The method setIteratorMode() 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

37
        $_root->value->/** @scrutinizer ignore-call */ 
38
                       setIteratorMode(NodeList::IT_MODE_DELETE);

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 setIteratorMode() 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

37
        $_root->value->/** @scrutinizer ignore-call */ 
38
                       setIteratorMode(NodeList::IT_MODE_DELETE);

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...
38
        foreach ($_root->value as $child) {
39
            if ($child->type & Y::DOC_START) {
40
                if(++$totalDocStart > 1){
41
                    $documents[] = self::buildDocument($buffer, count($documents));
0 ignored issues
show
Bug Best Practice introduced by
The method Dallgoot\Yaml\Builder::buildDocument() is not static, but was called statically. ( Ignorable by Annotation )

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

41
                    /** @scrutinizer ignore-call */ 
42
                    $documents[] = self::buildDocument($buffer, count($documents));
Loading history...
42
                    $buffer = new NodeList($child);
43
                }
44
            } else {
45
                $buffer->push($child);
46
            }
47
        }
48
        $documents[] = self::buildDocument($buffer, count($documents));
49
        return count($documents) === 1 ? $documents[0] : $documents;
50
    }
51
52
    private function buildDocument(NodeList $list, int $docNum):YamlObject
53
    {
54
        self::$_root = new YamlObject;
55
        try {
56
            $out = self::buildNodeList($list, self::$_root);
57
        } catch (\Exception $e) {
58
            throw new \ParseError(sprintf(self::INVALID_DOCUMENT, $docNum));
59
        }
60
        return $out;
61
    }
62
63
    /**
64
     * Generic function to distinguish between Node and NodeList
65
     *
66
     * @param Node|NodeList $node   The node.
67
     * @param mixed         $parent The parent
68
     *
69
     * @return mixed  ( description_of_the_return_value )
70
     */
71
    private static function build(object $node, &$parent = null)
72
    {
73
        if ($node instanceof NodeList) return self::buildNodeList($node, $parent);
74
        return self::buildNode($node, $parent);
75
    }
76
77
    /**
78
     * Builds a node list.
79
     *
80
     * @param NodeList $node   The node
81
     * @param mixed    $parent The parent
82
     *
83
     * @return mixed    The parent (object|array) or a string representing the NodeList.
84
     */
85
    private static function buildNodeList(NodeList $node, &$parent=null)
86
    {
87
        $node->forceType();
88
        if ($node->type & (Y::RAW | Y::LITTERALS)) {
89
            return self::buildLitteral($node, (int) $node->type);
90
        }
91
        $action = function ($child, &$parent, &$out) {
92
            self::build($child, $out);
93
        };
94
        if ($node->type & (Y::COMPACT_MAPPING|Y::MAPPING|Y::SET)) {
95
            $out = $parent ?? new \StdClass;
96
        } elseif ($node->type & (Y::COMPACT_SEQUENCE|Y::SEQUENCE)) {
97
            $out = $parent ?? [];
98
        } else {
99
            $out = '';
100
            $action = function ($child, &$parent, &$out) {
101
                if ($child->type & (Y::SCALAR|Y::QUOTED)) {
102
                    if ($parent) {
103
                        $parent->setText(self::build($child));
104
                    } else {
105
                        $out .= self::build($child);
106
                    }
107
                }
108
            };
109
        }
110
        foreach ($node as $child) {
111
            $action($child, $parent, $out);
112
        }
113
        if ($node->type & (Y::COMPACT_SEQUENCE|Y::COMPACT_MAPPING)) {
114
            $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

114
            $out = new Compact(/** @scrutinizer ignore-type */ $out);
Loading history...
115
        }
116
        return is_null($out) ? $parent : $out;
117
    }
118
119
    /**
120
     * Builds a node.
121
     *
122
     * @param Node    $node    The node of any Node->type
123
     * @param mixed  $parent  The parent
124
     *
125
     * @return mixed  The node value as Scalar, Array, Object or Null otherwise.
126
     */
127
    private static function buildNode(Node $node, &$parent)
128
    {
129
        extract((array) $node, EXTR_REFS);
130
        $actions = [Y::DIRECTIVE => 'buildDirective',
131
                    Y::ITEM      => 'buildItem',
132
                    Y::KEY       => 'buildKey',
133
                    Y::SET_KEY   => 'buildSetKey',
134
                    Y::SET_VALUE => 'buildSetValue',
135
                    Y::TAG       => 'buildTag',
136
        ];
137
        if (isset($actions[$type])) {
138
            return self::{$actions[$type]}($node, $parent);
139
        } elseif ($type & Y::COMMENT) {
140
            self::$_root->addComment($line, $value);
141
        } elseif ($type & (Y::COMPACT_MAPPING|Y::COMPACT_SEQUENCE)) {
142
            return self::buildNodeList($value, $parent);
143
        } elseif ($type & (Y::REF_DEF | Y::REF_CALL)) {
144
            return self::handleReference($node, $parent);
145
        } elseif ($value instanceof Node) {
146
            return self::buildNode($value, $parent);
147
        } else {
148
            return Node2PHP::get($node);
149
        }
150
    }
151
152
    private static function handleReference($node, $parent)
153
    {
154
        $tmp = is_null($node->value) ? null : self::build($node->value, $parent);
155
        if ($node->type === Y::REF_DEF) self::$_root->addReference($node->identifier, $tmp);
156
        return self::$_root->getReference($node->identifier);
157
    }
158
159
160
    /**
161
     * Builds a key and set the property + value to the given parent
162
     *
163
     * @param Node $node       The node with type YAML::KEY
164
     * @param object|array $parent       The parent
165
     *
166
     * @throws \ParseError if Key has no name(identifier) Note: empty string is allowed
167
     * @return null
168
     */
169
    private static function buildKey(Node $node, &$parent=null)
170
    {
171
        extract((array) $node, EXTR_REFS);
172
        if (is_null($identifier)) {
173
            throw new \ParseError(sprintf(self::ERROR_NO_KEYNAME, $line));
174
        } else {
175
            if ($value instanceof Node) {
176
                if ($value->type & (Y::ITEM|Y::KEY)) {
177
                    $value = new NodeList($value);
178
                } else {
179
                    $result = self::build($value);
180
                }
181
            }
182
            if ($value instanceof NodeList) {
183
                $result = self::buildNodeList($value);
184
            }
185
            if (is_null($parent)) {
186
                return $result;
187
            } else {
188
                if (is_array($parent)) {
189
                    $parent[$identifier] = $result;
190
                } else {
191
                    $parent->{$identifier} = $result;
192
                }
193
            }
194
        }
195
    }
196
197
    /**
198
     * Builds an item. Adds the item value to the parent array|Iterator
199
     *
200
     * @param      Node        $node    The node with type YAML::ITEM
201
     * @param      array|\Iterator      $parent  The parent
202
     *
203
     * @throws     \Exception  if parent is another type than array or object Iterator
204
     * @return null
205
     */
206
    private static function buildItem(Node $node, &$parent)
207
    {
208
        extract((array) $node, EXTR_REFS);
209
        if (!is_array($parent) && !($parent instanceof \ArrayIterator)) {
210
            throw new \Exception("parent must be an Iterable not ".(is_object($parent) ? get_class($parent) : gettype($parent)), 1);
211
        }
212
        $ref = $parent instanceof \ArrayIterator ? $parent->getArrayCopy() : $parent;
0 ignored issues
show
introduced by
$parent is never a sub-type of ArrayIterator.
Loading history...
213
        $numKeys = array_filter(array_keys($ref), 'is_int');
214
        $key = count($numKeys) > 0 ? max($numKeys) + 1 : 0;
215
        if ($value instanceof Node) {
216
            if($value->type & Y::KEY) {
217
                self::buildKey($node->value, $parent);
218
                return;
219
            } elseif ($value->type & Y::ITEM) {
220
                $a = [];
221
                $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...
222
            }
223
        }
224
        $result = self::build($value);
225
        $parent[$key] = $result;
226
    }
227
228
229
    /**
230
     * Builds a litteral (folded or not) or any NodeList that has YAML::RAW type (like a multiline value)
231
     *
232
     * @param      NodeList  $children  The children
233
     * @param      integer   $type      The type
234
     *
235
     * @return     string    The litteral.
236
     * @todo : Example 6.1. Indentation Spaces  spaces must be considered as content
237
     */
238
    private static function buildLitteral(NodeList $list, int $type = Y::RAW):string
239
    {
240
        $list->rewind();
241
        $refIndent = $list->current()->indent;
242
        //remove trailing blank
243
        while ($list->top()->type & Y::BLANK) $list->pop();
244
        $result = '';
245
        $separator = [ Y::RAW => '', Y::LITT => "\n", Y::LITT_FOLDED => ' '][$type];
246
        foreach ($list as $child) {
247
            if ($child->value instanceof NodeList) {
248
                $result .= self::buildLitteral($child->value, $type).$separator;
249
            } else {
250
                self::setLiteralValue($child, $result, $refIndent, $separator, $type);
0 ignored issues
show
Bug Best Practice introduced by
The method Dallgoot\Yaml\Builder::setLiteralValue() is not static, but was called statically. ( Ignorable by Annotation )

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

250
                self::/** @scrutinizer ignore-call */ 
251
                      setLiteralValue($child, $result, $refIndent, $separator, $type);
Loading history...
251
            }
252
        }
253
        return rtrim($result);
254
    }
255
256
    private function setLiteralValue(Node $child, string &$result, int $refIndent, string $separator, int $type)
257
    {
258
        $val = $child->type & (Y::SCALAR) ? $child->value : substr($child->raw, $refIndent);
259
        if ($type & Y::LITT_FOLDED && ($child->indent > $refIndent || ($child->type & Y::BLANK))) {
260
            if ($result[-1] === $separator)
261
                $result[-1] = "\n";
262
            if ($result[-1] === "\n")
263
                $result .= $val;
264
            return;
265
        }
266
        $result .= $val.$separator;
267
    }
268
269
    /**
270
     * Builds a set key.
271
     *
272
     * @param      Node        $node    The node of type YAML::SET_KEY.
273
     * @param      object      $parent  The parent
274
     *
275
     * @throws     \Exception  if a problem occurs during serialisation (json format) of the key
276
     */
277
    private function buildSetKey(Node $node, &$parent)
278
    {
279
        $built = is_object($node->value) ? self::build($node->value) : null;
280
        $stringKey = is_string($built) && Regex::isProperlyQuoted($built) ? trim($built, '\'" '): $built;
281
        $key = json_encode($stringKey, JSON_PARTIAL_OUTPUT_ON_ERROR|JSON_UNESCAPED_SLASHES);
282
        if (empty($key)) throw new \Exception("Cant serialize complex key: ".var_export($node->value, true), 1);
283
        $parent->{trim($key, '\'" ')} = null;
284
    }
285
286
    /**
287
     * Builds a set value.
288
     *
289
     * @param      Node    $node    The node of type YAML::SET_VALUE
290
     * @param      object  $parent  The parent (the document object or any previous object created through a mapping key)
291
     */
292
    private function buildSetValue(Node $node, &$parent)
293
    {
294
        $prop = array_keys(get_object_vars($parent));
295
        $key = end($prop);
296
        if ($node->value->type & (Y::ITEM|Y::KEY )) {
297
            $node->value = new NodeList($node->value);
298
        }
299
        $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

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