Passed
Push — master ( 6b8e06...02b784 )
by stéphane
04:35
created

Builder::buildSetValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 2
dl 0
loc 11
rs 10
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
/**
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
 */
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...
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
        if ($_root->value instanceof Node) {
37
            $q = new NodeList;
38
            $q->push($_root->value);
39
            // return self::buildNodeList($q, new YamlObject);
40
            self::$_root = new YamlObject;
41
            $tmp =  self::buildNodeList($q, self::$_root);
42
            // var_dump('alone', $tmp);
43
            return $tmp;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $tmp also could return the type Dallgoot\Yaml\Compact|StdClass|string which is incompatible with the documented return type Dallgoot\Yaml\YamlObject|array.
Loading history...
44
        }
45
        $_root->value instanceof NodeList && $_root->value->setIteratorMode(NodeList::IT_MODE_DELETE);
46
        foreach ($_root->value as $child) {
47
            if ($child->type & Y::DOC_START) $totalDocStart++;
48
            //if 0 or 1 DOC_START = we are still in first document
49
            $currentDoc = $totalDocStart > 1 ? $totalDocStart - 1 : 0;
50
            if (!isset($documents[$currentDoc])) $documents[$currentDoc] = new NodeList();
51
            $documents[$currentDoc]->push($child);
52
        }
53
        // $content = array_map([self::class, 'buildDocument'], $documents, array_keys($documents));
54
        $content = [];
55
        foreach ($documents as $num => $list) {
56
            try {
57
                self::$_root = new YamlObject;
58
                // $tmp = var_dump('insideforeach'.$tmp);
59
                $content[] = self::buildNodeList($list, self::$_root);
60
            } catch (\Exception $e) {
61
                throw new \ParseError(sprintf(self::INVALID_DOCUMENT, $num));
62
            }
63
        }
64
        // $content = array_map([self::class, 'buildNodeList'], $documents, array_keys($documents));
65
        return count($content) === 1 ? $content[0] : $content;
66
    }
67
68
    /**
69
     * Generic function to distinguish between Node and NodeList
70
     *
71
     * @param Node|NodeList $node   The node.
72
     * @param mixed         $parent The parent
73
     *
74
     * @return mixed  ( description_of_the_return_value )
75
     */
76
    private static function build(object $node, &$parent = null)
77
    {
78
        if ($node instanceof NodeList) return self::buildNodeList($node, $parent);
79
        return self::buildNode($node, $parent);
80
    }
81
82
    /**
83
     * Builds a node list.
84
     *
85
     * @param NodeList $node   The node
86
     * @param mixed    $parent The parent
87
     *
88
     * @return mixed    The parent (object|array) or a string representing the NodeList.
89
     */
90
    private static function buildNodeList(NodeList $node, &$parent=null)
0 ignored issues
show
Coding Style introduced by
Private method name "Builder::buildNodeList" must be prefixed with an underscore
Loading history...
91
    {
92
        if (is_null($node->type)) {
93
            $childTypes  = $node->getTypes();
94
            if ($childTypes & (Y::KEY|Y::SET_KEY)) {
95
                if ($childTypes & Y::ITEM) {
96
                    // TODO: replace the document index in HERE ----------v
97
                    throw new \ParseError(sprintf(self::INVALID_DOCUMENT, 0));
98
                } else {
99
                    $node->type = Y::MAPPING;
100
                }
101
            } else {
102
                if ($childTypes & Y::ITEM) {
103
                    $node->type = Y::SEQUENCE;
104
                } elseif (!($childTypes & Y::COMMENT)) {
105
                    $node->type = Y::LITT_FOLDED;
106
                }
107
            }
108
        }
109
        if ($node->type & (Y::RAW | Y::LITTERALS)) {
110
            return self::buildLitteral($node, (int) $node->type);
111
        }
112
        $action = function ($child, &$parent, &$out) {
113
            self::build($child, $out);
114
        };
115
        if ($node->type & (Y::COMPACT_MAPPING|Y::MAPPING|Y::SET)) {
116
            $out = $parent ?? new \StdClass;
117
        } elseif ($node->type & (Y::COMPACT_SEQUENCE|Y::SEQUENCE)) {
118
            $out = $parent ?? [];
119
        } else {
120
            $out = '';
121
            $action = function ($child, &$out) {
122
                if ($child->type & (Y::SCALAR|Y::QUOTED)) {
123
                    if ($parent) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $parent seems to be never defined.
Loading history...
124
                        $parent->setText(self::build($child));
125
                    } else {
126
                        $out .= self::build($child);
127
                    }
128
                }
129
            };
130
        }
131
        foreach ($node as $child) {
132
            $action($child, $parent, $out);
133
        }
134
        if ($node->type & (Y::COMPACT_SEQUENCE|Y::COMPACT_MAPPING)) {
135
            $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

135
            $out = new Compact(/** @scrutinizer ignore-type */ $out);
Loading history...
136
        }
137
        return is_null($out) ? $parent : $out;
138
    }
139
140
    /**
141
     * Builds a node.
142
     *
143
     * @param Node    $node    The node of any Node->type
144
     * @param mixed  $parent  The parent
145
     *
146
     * @return mixed  The node value as scalar, array or object or null to otherwise.
147
     */
148
    private static function buildNode(Node $node, &$parent)
0 ignored issues
show
Coding Style introduced by
Private method name "Builder::buildNode" must be prefixed with an underscore
Loading history...
149
    {
150
        extract((array) $node, EXTR_REFS);
151
        if ($type & (Y::REF_DEF | Y::REF_CALL)) {
152
            if (is_object($value)) {
153
                $tmp = self::build($value, $parent) ?? $parent;
154
            } else {
155
                $tmp = Node2PHP::get($node);
156
            }
157
            if ($type === Y::REF_DEF) self::$_root->addReference($identifier, $tmp);
158
            return self::$_root->getReference($identifier);
159
        }
160
        if ($type & (Y::COMPACT_MAPPING|Y::COMPACT_SEQUENCE)) {
161
            return self::buildNodeList($node->value, $parent);
162
        }
163
        if ($type & Y::COMMENT) self::$_root->addComment($node->line, $node->value);
164
        $typesActions = [Y::DIRECTIVE => 'buildDirective',
165
                         Y::ITEM      => 'buildItem',
166
                         Y::KEY       => 'buildKey',
167
                         Y::SET_KEY   => 'buildSetKey',
168
                         Y::SET_VALUE => 'buildSetValue',
169
                         Y::TAG       => 'buildTag',
170
        ];
171
        if (isset($typesActions[$type])) {
172
            return self::{$typesActions[$type]}($node, $parent);
173
        }
174
        return is_object($value) ? self::build($value, $parent) : Node2PHP::get($node);
175
    }
176
177
    /**
178
     * Builds a key and set the property + value to the given parent
179
     *
180
     * @param Node $node       The node with type YAML::KEY
181
     * @param object|array $parent       The parent
182
     *
183
     * @throws \ParseError if Key has no name(identifier) Note: empty string is allowed
184
     * @return null
185
     */
186
    private static function buildKey(Node $node, &$parent=null)
0 ignored issues
show
Coding Style introduced by
Private method name "Builder::buildKey" must be prefixed with an underscore
Loading history...
187
    {
188
        extract((array) $node, EXTR_REFS);
189
        if (is_null($identifier)) {
190
            throw new \ParseError(sprintf(self::ERROR_NO_KEYNAME, $line));
191
        } else {
192
            if ($value instanceof Node) {
193
                if ($value->type & (Y::ITEM|Y::KEY)) {
194
                    $list = new NodeList();
195
                    $list->push($value);
196
                    $list->type = $value->type & Y::ITEM ? Y::SEQUENCE : Y::MAPPING;
197
                    $value = $list;
198
                } else {
199
                    $result = self::build($value);
200
                }
201
            }
202
            if ($value instanceof NodeList) {
203
                $childTypes = $value->getTypes();
204
                if (is_null($value->type) && $childTypes & Y::SCALAR && !($childTypes & Y::COMMENT)) {
205
                    $result = self::buildLitteral($value, Y::LITT_FOLDED);
206
                } else {
207
                    $result = self::buildNodeList($value);
208
                }
209
            }
210
            if (is_null($parent)) {
211
                return $result;
212
            } else {
213
                if (is_array($parent)) {
214
                    $parent[$identifier] = $result;
215
                } else {
216
                    $parent->{$identifier} = $result;
217
                }
218
            }
219
        }
220
    }
221
222
    /**
223
     * Builds an item. Adds the item value to the parent array|Iterator
224
     *
225
     * @param      Node        $node    The node with type YAML::ITEM
226
     * @param      array|\Iterator      $parent  The parent
227
     *
228
     * @throws     \Exception  if parent is another type than array or object Iterator
229
     * @return null
230
     */
231
    private static function buildItem(Node $node, &$parent)
0 ignored issues
show
Coding Style introduced by
Private method name "Builder::buildItem" must be prefixed with an underscore
Loading history...
232
    {
233
        extract((array) $node, EXTR_REFS);//var_dump(__METHOD__);
234
        if (!is_array($parent) && !($parent instanceof \ArrayIterator)) {
235
            throw new \Exception("parent must be an Iterable not ".(is_object($parent) ? get_class($parent) : gettype($parent)), 1);
236
        }
237
        $ref = $parent instanceof \ArrayIterator ? $parent->getArrayCopy() : $parent;
0 ignored issues
show
introduced by
$parent is never a sub-type of ArrayIterator.
Loading history...
238
        $numKeys = array_filter(array_keys($ref), 'is_int');
239
        $key = count($numKeys) > 0 ? max($numKeys) + 1 : 0;
240
        if ($value instanceof Node) {
241
            if($value->type & Y::KEY) {
0 ignored issues
show
Coding Style introduced by
Expected "if (...) {\n"; found "if(...) {\n"
Loading history...
242
                self::buildKey($node->value, $parent);
243
                return;
244
            } elseif ($value->type & Y::ITEM) {
245
                $list = new NodeList();
246
                $list->push($value);
247
                $list->type = Y::SEQUENCE;
248
                $result = self::buildNodeList($list);
249
            } else {
250
                $result = self::build($value);
251
            }
252
        } elseif ($value instanceof NodeList) {
253
            $result = self::buildNodeList($value);
254
        }
255
        $parent[$key] = $result;
256
    }
257
258
259
    /**
260
     * Builds a litteral (folded or not) or any NodeList that has YAML::RAW type (like a multiline value)
261
     *
262
     * @param      NodeList  $children  The children
263
     * @param      integer   $type      The type
264
     *
265
     * @return     string    The litteral.
266
     */
267
    private static function buildLitteral(NodeList $children, int $type):string
0 ignored issues
show
Coding Style introduced by
Private method name "Builder::buildLitteral" must be prefixed with an underscore
Loading history...
268
    {
269
        $lines = [];
270
        $children->rewind();
271
        $refIndent = $children->current()->indent;
272
        //remove trailing blank nodes
273
        $max = $children->count() - 1;
274
        while ($children->offsetGet($max)->type & Y::BLANK) {
275
            $children->offsetUnset($max);
276
            $max = $children->count() - 1;
277
        }
278
        $children->rewind();
279
        // TODO : Example 6.1. Indentation Spaces  spaces must be considered as content
280
        foreach ($children as $child) {
281
            if ($child->value instanceof NodeList) {
282
                $lines[] = self::buildLitteral($child->value, $type);
283
            } else {
284
                $prefix = '';
285
                if ($type & Y::LITT_FOLDED && ($child->indent > $refIndent || ($child->type & Y::BLANK))) {
286
                    $prefix = "\n";
287
                }
288
                if (!($child->type & (Y::SCALAR|Y::BLANK))) {
289
                    switch ($child->type) {
290
                        case Y::ITEM:    $child->value = '- '.$child->value;break;
0 ignored issues
show
Coding Style introduced by
Closing brace must be on a line by itself
Loading history...
291
                        case Y::COMMENT: $child->value = '# '.$child->value;break;
0 ignored issues
show
Coding Style introduced by
Closing brace must be on a line by itself
Loading history...
292
                        default: //die(__METHOD__.Y::getName($child->type));
293
                    }
294
                }
295
                $val = $child->value;
296
                while (is_object($val)) {
297
                    $val = $val->value;
298
                }
299
                $lines[] = $prefix.trim($val);
300
            }
301
        }
302
        if ($type & Y::RAW)         return implode('',   $lines);
303
        if ($type & Y::LITT)        return implode("\n", $lines);
304
        if ($type & Y::LITT_FOLDED) return preg_replace(['/ +(\n)/','/(\n+) +/'], "$1", implode(' ',  $lines));
305
        // TODO : rewrite without 'preg_replace' if ($type & Y::LITT_FOLDED) return implode(' ',  $lines);
306
        return '';
307
    }
308
309
    /**
310
     * Builds a set key.
311
     *
312
     * @param      Node        $node    The node of type YAML::SET_KEY.
313
     * @param      object      $parent  The parent
314
     *
315
     * @throws     \Exception  if a problem occurs during serialisation (json format) of the key
316
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
317
    private function buildSetKey(Node $node, &$parent)
0 ignored issues
show
Coding Style introduced by
Private method name "Builder::buildSetKey" must be prefixed with an underscore
Loading history...
318
    {
319
        $built = is_object($node->value) ? self::build($node->value) : null;
320
        $stringKey = is_string($built) && Regex::isProperlyQuoted($built) ? trim($built, '\'" '): $built;
321
        $key = json_encode($stringKey, JSON_PARTIAL_OUTPUT_ON_ERROR|JSON_UNESCAPED_SLASHES);
322
        // if (empty($key)) throw new \Exception("Cant serialize complex key: ".var_export($node->value, true), 1);
323
        $parent->{trim($key, '\'" ')} = null;
324
    }
325
326
    /**
327
     * Builds a set value.
328
     *
329
     * @param      Node    $node    The node of type YAML::SET_VALUE
330
     * @param      object  $parent  The parent (the document object or any previous object created through a mapping key)
331
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
332
    private function buildSetValue(Node $node, &$parent)
0 ignored issues
show
Coding Style introduced by
Private method name "Builder::buildSetValue" must be prefixed with an underscore
Loading history...
333
    {
334
        $prop = array_keys(get_object_vars($parent));
335
        $key = end($prop);
336
        if ($node->value->type & (Y::ITEM|Y::KEY|Y::SEQUENCE|Y::MAPPING)) {
337
            $p = $node->value->type === Y::ITEM ? [] : new \StdClass;
338
            self::build($node->value, $p);
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

338
            self::build(/** @scrutinizer ignore-type */ $node->value, $p);
Loading history...
339
        } else {
340
            $p = self::build($node->value, $parent->{$key});
341
        }
342
        $parent->{$key} = $p;
343
    }
344
345
    /**
346
     * Builds a tag and its value (also built) and encapsulates them in a Tag object.
347
     *
348
     * @param      Node    $node    The node of type YAML::TAG
349
     * @param      mixed  $parent  The parent
350
     *
351
     * @return     Tag|null     The tag object of class Dallgoot\Yaml\Tag.
352
     */
353
    private static function buildTag(Node $node, &$parent)
354
    {
355
        $name = (string) $node->identifier;
356
        if ($parent === self::$_root && empty($node->value)) {
357
            $parent->addTag($name);
358
            return;
359
        }
360
        $target = $node->value;
361
        if ($node->value instanceof Node) {
362
            if ($node->value->type & (Y::KEY|Y::ITEM)) {
363
                if (is_null($parent)) {
364
                    $target = new NodeList;
365
                    $target->push($node->value);
366
                    $target->type = $node->value->type & Y::KEY ? Y::MAPPING : Y::SEQUENCE;
367
                } else {
368
                    $node->value->type & Y::KEY ? self::buildKey($node->value, $parent) : self::buildItem($node->value, $parent);
369
                }
370
            }
371
        }
372
        //TODO: have somewhere a list of common tags and their treatment
373
        // if (in_array($node->identifier, ['!binary', '!str'])) {
374
        //     $target->type = Y::RAW;
375
        // }
376
        return new Tag($name, is_object($target) ? self::build($target) : null);
377
    }
378
379
    /**
380
     * Builds a directive. NOT IMPLEMENTED YET
381
     *
382
     * @param      Node  $node    The node
383
     * @param      mixed  $parent  The parent
384
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
385
    private function buildDirective(Node $node, $parent)
0 ignored issues
show
Coding Style introduced by
Private method name "Builder::buildDirective" must be prefixed with an underscore
Loading history...
386
    {
387
        // TODO : implement
388
    }
389
}
390