Passed
Push — master ( 42571c...f8295f )
by stéphane
09:20
created

Loader::parse()   B

Complexity

Conditions 8
Paths 46

Size

Total Lines 23
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 8

Importance

Changes 0
Metric Value
cc 8
eloc 19
nc 46
nop 1
dl 0
loc 23
ccs 19
cts 19
cp 1
crap 8
rs 8.4444
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace Dallgoot\Yaml;
5
6
use Dallgoot\Yaml\Nodes;
7
8
/**
9
 * Process reading a Yaml Content (loading file if required)
10
 * and for each line affects appropriate NodeType
11
 * and attach to proper parent Node
12
 * ie. constructs a tree of Nodes with a NodeRoot as first Node
13
 *
14
 * @author  Stéphane Rebai <[email protected]>
15
 * @license Apache 2.0
16
 * @link    https://github.com/dallgoot/yaml
17
 */
18
final class Loader
19
{
20
    //public
21
    /* @var null|string */
22
    public static $error;
23
    public const IGNORE_DIRECTIVES     = 0b0001;//DONT include_directive
24
    public const IGNORE_COMMENTS       = 0b0010;//DONT include_comments
25
    public const NO_PARSING_EXCEPTIONS = 0b0100;//DONT throw Exception on parsing errors
26
    public const NO_OBJECT_FOR_DATE    = 0b1000;//DONT import date strings as dateTime Object
27
28
    //private
29
    /* @var null|array */
30
    private $content;
31
    /* @var null|string */
32
    private $filePath;
33
    /* @var integer */
34
    private $_debug = 0;
35
    /* @var integer */
36
    private $_options = 0;
37
    /* @var array */
38
    private $_blankBuffer = [];
39
40
    //Exceptions messages
41
    private const INVALID_VALUE        = self::class.": at line %d";
42
    private const EXCEPTION_NO_FILE    = self::class.": file '%s' does not exists (or path is incorrect?)";
43
    private const EXCEPTION_READ_ERROR = self::class.": file '%s' failed to be loaded (permission denied ?)";
44
    private const EXCEPTION_LINE_SPLIT = self::class.": content is not a string (maybe a file error?)";
45
46
    /**
47
     * Loader constructor
48
     *
49
     * @param string|null       $absolutePath The file absolute path
50
     * @param int|null          $options      The options (bitmask as int value)
51
     * @param integer|bool|null $debug        The debug level as either boolean (true=1) or any integer
52
     */
53 12
    public function __construct($absolutePath = null, $options = null, $debug = 0)
54
    {
55 12
        $this->_debug   = is_null($debug) ? 0 : min($debug, 3);
56 12
        $this->_options = is_int($options) ? $options : $this->_options;
57 12
        if (is_string($absolutePath)) {
58 1
            $this->load($absolutePath);
59
        }
60 12
    }
61
62
    /**
63
     * Load a file and save its content as $content
64
     *
65
     * @param string $absolutePath The absolute path of a file
66
     *
67
     * @throws \Exception if file don't exist OR reading failed
68
     *
69
     * @return self  ( returns the same Loader  )
70
     */
71 3
    public function load(string $absolutePath):Loader
72
    {
73 3
        if (!file_exists($absolutePath)) {
74 2
            throw new \Exception(sprintf(self::EXCEPTION_NO_FILE, $absolutePath));
75
        }
76 1
        $this->filePath = $absolutePath;
77 1
        $adle_setting = "auto_detect_line_endings";
78 1
        ini_set($adle_setting, "true");
79 1
        $content = @file($absolutePath, FILE_IGNORE_NEW_LINES);
80 1
        ini_restore($adle_setting);
81 1
        if (is_bool($content)) {
82
            throw new \Exception(sprintf(self::EXCEPTION_READ_ERROR, $absolutePath));
83
        }
84 1
        $this->content = $content;
85 1
        return $this;
86
    }
87
88
    /**
89
     * Gets the source iterator.
90
     *
91
     * @param string|null $strContent  The string content
92
     *
93
     * @throws \Exception if self::content is empty or splitting on linefeed has failed
94
     * @return \Generator  The source iterator.
95
     */
96 4
    private function getSourceGenerator($strContent = null):\Generator
97
    {
98 4
        if (is_null($strContent) && is_null($this->content)) {
99 1
            throw new \Exception(self::EXCEPTION_LINE_SPLIT);
100
        }
101 3
        if (!is_null($this->content)) {
102 1
            $source = $this->content;
103
        } else {
104 2
            $simplerLineFeeds = preg_replace('/(\r\n|\r)$/', "\n", (string) $strContent);
105 2
            $source = preg_split("/\n/m", $simplerLineFeeds, 0, \PREG_SPLIT_DELIM_CAPTURE);
106
        }
107 3
        if (!is_array($source) || !count($source)) {
108 1
            throw new \Exception(self::EXCEPTION_LINE_SPLIT);
109
        }
110 2
        foreach ($source as $key => $value) {
111 2
            yield ++$key => $value;
112
        }
113 2
    }
114
115
    /**
116
     * Parse Yaml lines into a hierarchy of Node
117
     *
118
     * @param string $strContent The Yaml string or null to parse loaded content
119
     *
120
     * @throws \Exception    if content is not available as $strContent or as $this->content (from file)
121
     * @throws \ParseError  if any error during parsing or building
122
     *
123
     * @return array|YamlObject|null      null on errors if NO_PARSING_EXCEPTIONS is set, otherwise an array of YamlObject or just YamlObject
124
     */
125 2
    public function parse($strContent = null)
126
    {
127 2
        $generator = $this->getSourceGenerator($strContent);
128 2
        $previous = $root = new Nodes\Root();
129
        try {
130 2
            foreach ($generator as $lineNB => $lineString) {
131 2
                $node = NodeFactory::get($lineString, $lineNB);
132 2
                if ($this->_debug === 1) echo $lineNB.":".get_class($node)."\n";
133 2
                if ($this->needsSpecialProcess($node, $previous)) continue;
134 2
                $this->_attachBlankLines($previous);
135 2
                switch ($node->indent <=> $previous->indent) {
136 1
                    case -1: $target = $previous->getTargetOnLessIndent($node);
137 1
                        break;
138 2
                    case 0:  $target = $previous->getTargetOnEqualIndent($node);
139 1
                        break;
140 2
                    default: $target = $previous->getTargetOnMoreIndent($node);
141
                }
142 2
                $previous = $target->add($node);
143
            }
144 2
            $this->_attachBlankLines($previous);
145 2
            return $this->_debug === 1 ? null : (new Builder($this->_options, $this->_debug))->buildContent($root);
146 1
        } catch (\Throwable $e) {
147 1
            $this->onError($e, $lineNB);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $lineNB seems to be defined by a foreach iteration on line 130. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
148
        }
149
    }
150
151
152
    /**
153
     * Attach blank (empty) Nodes saved in $_blankBuffer to their parent (it means they are meaningful content)
154
     *
155
     * @param nodes\NodeGeneric  $previous   The previous Node
156
     *
157
     * @return null
158
     */
159 3
    private function _attachBlankLines(Nodes\NodeGeneric $previous)
160
    {
161 3
        foreach ($this->_blankBuffer as $blankNode) {
162 1
            if ($blankNode !== $previous) {
163 1
                $blankNode->getParent()->add($blankNode);
164
            }
165
        }
166 3
        $this->_blankBuffer = [];
167 3
    }
168
169
    /**
170
     * For certain (special) Nodes types some actions are required BEFORE parent assignment
171
     *
172
     * @param Nodes\NodeGeneric   $previous   The previous Node
173
     *
174
     * @return boolean  if True self::parse skips changing previous and adding to parent
175
     * @see self::parse
176
     */
177 3
    private function needsSpecialProcess(Nodes\NodeGeneric $current, Nodes\NodeGeneric $previous):bool
178
    {
179 3
        $deepest = $previous->getDeepestNode();
180 3
        if ($deepest instanceof Nodes\Partial) {
181 1
            return $deepest->specialProcess($current,  $this->_blankBuffer);
182 3
        } elseif(!($current instanceof Nodes\Partial)) {
183 3
            return $current->specialProcess($previous, $this->_blankBuffer);
184
        }
185 1
        return false;
186
    }
187
188
    // private function onError(\Throwable $e, \Generator $generator)
189 2
    private function onError(\Throwable $e, int $lineNB)
190
    {
191 2
        $file = $this->filePath ? realpath($this->filePath) : '#YAML STRING#';
192 2
        $message = $e->getMessage()."\n ".$e->getFile().":".$e->getLine();
193 2
        if ($this->_options & self::NO_PARSING_EXCEPTIONS) {
194 1
            self::$error = $message;
195 1
            return null;
196
        }
197 1
        throw new \Exception($message." for $file:".$lineNB, 1, $e);
198
    }
199
}
200