Test Failed
Push — master ( 44c6e4...f07b2d )
by stéphane
03:54 queued 11s
created

Loader::getSourceIterator()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
cc 4
nc 2
nop 1
1
<?php
2
declare(strict_types=1);
3
4
namespace Dallgoot\Yaml;
5
6
use Dallgoot\Yaml\Yaml as Y;
7
8
/**
9
 * TODO
10
 *
11
 * @author  Stéphane Rebai <[email protected]>
12
 * @license Apache 2.0
13
 * @link    TODO : url to specific online doc
14
 */
15
final class Loader
16
{
17
    //public
18
    /* @var null|string */
19
    public static $error;
20
    public const IGNORE_DIRECTIVES = 1;//DONT include_directive
21
    public const IGNORE_COMMENTS    = 2;//DONT include_comments
22
    public const NO_PARSING_EXCEPTIONS = 4;//DONT throw Exception on parsing errors
23
    public const NO_OBJECT_FOR_DATE = 8;//DONT import date strings as dateTime Object
24
25
    //privates
26
    /* @var null|false|array */
27
    private $content;
28
    /* @var null|string */
29
    private $filePath;
30
    /* @var integer */
31
    private $debug = 0;///TODO: determine levels
32
    /* @var integer */
33
    private $options = 0;
34
    //Exceptions messages
35
    private const INVALID_VALUE        = self::class.": at line %d";
36
    private const EXCEPTION_NO_FILE    = self::class.": file '%s' does not exists (or path is incorrect?)";
37
    private const EXCEPTION_READ_ERROR = self::class.": file '%s' failed to be loaded (permission denied ?)";
38
    private const EXCEPTION_LINE_SPLIT = self::class.": content is not a string(maybe a file error?)";
39
40
    public function __construct($absolutePath = null, $options = null, $debug = 0)
41
    {
42
        $this->debug   = is_int($debug) ? min($debug, 3) : 1;
43
        $this->options = is_int($options) ? $options : $this->options;
44
        if (is_string($absolutePath)) {
45
            $this->load($absolutePath);
46
        }
47
    }
48
49
    /**
50
     * Load a file and save its content as $content
51
     *
52
     * @param string $absolutePath The absolute path of a file
53
     *
54
     * @throws \Exception if file don't exist OR reading failed
55
     *
56
     * @return self  ( returns the same Loader  )
57
     */
58
    public function load(string $absolutePath):Loader
59
    {
60
        if (!file_exists($absolutePath)) {
61
            throw new \Exception(sprintf(self::EXCEPTION_NO_FILE, $absolutePath));
62
        }
63
        $this->filePath = $absolutePath;
64
        $adle = "auto_detect_line_endings";
65
        $prevADLE = ini_get($adle);
66
        !$prevADLE && ini_set($adle, "true");
67
        $content = file($absolutePath, FILE_IGNORE_NEW_LINES);
68
        !$prevADLE && ini_set($adle, "false");
69
        if (is_bool($content)) {
70
            throw new \Exception(sprintf(self::EXCEPTION_READ_ERROR, $absolutePath));
71
        }
72
        $this->content = $content;
73
        return $this;
74
    }
75
76
    private function getSourceIterator($strContent = null)
77
    {
78
        $source = $this->content ?? preg_split("/\n/m", preg_replace('/(\r\n|\r)/', "\n", $strContent), 0, PREG_SPLIT_DELIM_CAPTURE);
79
        //TODO : be more permissive on $strContent values
80
        if (!is_array($source) || !count($source)) throw new \Exception(self::EXCEPTION_LINE_SPLIT);
81
        return function () use($source) {
82
            foreach ($source as $key => $value) {
83
                yield ++$key => $value;
84
            }
85
        };
86
    }
87
88
    /**
89
     * Parse Yaml lines into a hierarchy of Node
90
     *
91
     * @param string $strContent The Yaml string or null to parse loaded content
0 ignored issues
show
Documentation introduced by
Should the type for parameter $strContent not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
92
     *
93
     * @throws \Exception    if content is not available as $strContent or as $this->content (from file)
94
     * @throws \ParseError  if any error during parsing or building
95
     *
96
     * @return array|YamlObject|null      null on errors if NO_PARSING_EXCEPTIONS is set, otherwise an array of YamlObject or just YamlObject
97
     */
98
    public function parse($strContent = null)
99
    {
100
        $sourceIterator = $this->getSourceIterator($strContent);
101
        $previous = $root = new Node();
102
        $emptyLines = [];
103
        try {
104
            foreach ($sourceIterator() as $lineNb => $lineString) {
105
                $n = new Node($lineString, $lineNb);
106
                $deepest = $previous->getDeepestNode();
107
                if ($n->type & (Y::LITTERALS|Y::BLANK|Y::COMMENT|Y::TAG) || $deepest->type & Y::PARTIAL) {
108
                    if ($this->onSpecialType($n, $previous, $emptyLines, $lineString)) continue;
109
                }
110
                //Note: 6.6 comments: Note that outside scalar content, a line containing only white space characters is taken to be a comment line.
111
                foreach ($emptyLines as $blankNode) {
112
                    if ($blankNode !== $previous) {
113
                        $blankNode->getParent()->add($blankNode);
114
                    }
115
                }
116
                $emptyLines = [];
117
                switch ($n->indent <=> $previous->indent) {
118
                    case -1: $target = $previous->getParent($n->indent, $n->type & Y::ITEM ? Y::KEY : null);
119
                        break;
120
                    case 0:
121
                        if ($n->type & Y::KEY && $n->indent === 0) {
122
                            $target = $root;
123
                        } elseif($n->type & Y::ITEM && $deepest->type & Y::KEY && is_null($deepest->value)) {
124
                            $target = $deepest;
125
                        } else {
126
                            $target = $previous->getParent();
127
                        }
128
                        break;
129
                    default:
130
                        $target = $previous;
131
                        if ($previous->type & Y::ITEM) {
132
                            if ($deepest->type & (Y::KEY|Y::TAG) && is_null($deepest->value)) {
133
                                $target = $deepest;
134
                            }
135
                        }
136
                }
137
                if ($this->onContextType($n, $target, $lineString)) continue;
138
                $target->add($n);
139
                $previous = $n;
140
            }
141
            if ($this->debug === 2) {
142
                print_r((new \ReflectionClass(Y::class))->getConstants());
143
                echo "\033[33mParsed Structure\033[0m\n";
144
                print_r($root);
145
                exit();
146
            }
147
            $out = Builder::buildContent($root, $this->debug);
148
            return $out;
149
        } catch (\Error|\Exception|\ParseError $e) {
150
            $file = $this->filePath ? basename($this->filePath) : '#YAML STRING#';
151
            $message = basename($e->getFile())."@".$e->getLine().": ".$e->getMessage()." for '$file' @".($lineNb)."\n";
0 ignored issues
show
Bug introduced by
The variable $lineNb seems to be defined by a foreach iteration on line 104. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
152
            if ($this->options & self::NO_PARSING_EXCEPTIONS) {
153
                // trigger_error($message, E_USER_WARNING);
154
                self::$error = $message;
155
                return null;
156
            }
157
            $this->debug && print_r($root);
158
            throw new \Exception($message, 3);
159
        }
160
    }
161
162
    private function onSpecialType(Node &$n, Node &$previous, &$emptyLines, $lineString):bool
0 ignored issues
show
Coding Style introduced by
function onSpecialType() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
163
    {
164
        $deepest = $previous->getDeepestNode();
165
        if ($deepest->type & Y::PARTIAL) {
166
            $add = trim($lineString) === '' ? "\n" : trim($lineString);
167
            if ($add !== "\n" && $deepest->value[-1] !== "\n") {
168
                $add = ' '.$add;
169
            }
170
            $deepest->parse($deepest->value.$add);
171
            return true;
172
        }
173
        if ($n->type & Y::BLANK) {
174
            if ($previous->type & Y::SCALAR)   $emptyLines[] = $n->setParent($previous->getParent());
175
            if ($deepest->type & Y::LITTERALS) $emptyLines[] = $n->setParent($deepest);
176
            return true;
177
        } elseif ($n->type & Y::COMMENT
178
                  && !($previous->getParent()->value->type & Y::LITTERALS)
179
                  && !($deepest->type & Y::LITTERALS)) {
180
            $previous->getParent(0)->add($n);
181
            return true;
182
        } elseif ($n->type & Y::TAG && is_null($n->value) && $previous->type & (Y::ROOT|Y::DOC_START|Y::DOC_END)) {
183
            $n->value = '';
184
        }
185
        return false;
186
    }
187
188
    private function onContextType(Node &$n, Node &$previous, $lineString):bool
0 ignored issues
show
Coding Style introduced by
function onContextType() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
189
    {
190
        $deepest = $previous->getDeepestNode();
191
        if (($previous->type & Y::LITTERALS && $n->indent >= $previous->indent) || (($deepest->type & Y::LITTERALS) && is_null($deepest->value))) {
192
            $n->type = Y::SCALAR;
193
            $n->identifier = null;
194
            $n->value = trim($lineString);
195
            $previous = $deepest->getParent();
196
            return false;
197
        }
198
        if (is_null($deepest->value) && $deepest->type & (Y::LITTERALS|Y::REF_DEF|Y::SET_VALUE|Y::TAG)) {
199
            $previous = $deepest;
200
        }
201
        // if ($n->type & Y::SCALAR && $previous->type & Y::SCALAR) {
202
        //     $previous = $previous->getParent();
203
        // }
204
        return false;
205
    }
206
}
207