Passed
Branch master (09b7c0)
by stéphane
02:52
created

Loader::load()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5.0113

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 16
ccs 12
cts 13
cp 0.9231
rs 9.5555
c 0
b 0
f 0
cc 5
nc 9
nop 1
crap 5.0113
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     = 1;//DONT include_directive
24
    public const IGNORE_COMMENTS       = 2;//DONT include_comments
25
    public const NO_PARSING_EXCEPTIONS = 4;//DONT throw Exception on parsing errors
26
    public const NO_OBJECT_FOR_DATE    = 8;//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 11
    public function __construct($absolutePath = null, $options = null, $debug = 0)
54
    {
55 11
        $this->_debug   = is_null($debug) ? 0 : min($debug, 3);
56 11
        $this->_options = is_int($options) ? $options : $this->_options;
57 11
        if (is_string($absolutePath)) {
58 1
            $this->load($absolutePath);
59
        }
60 11
    }
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 = "auto_detect_line_endings";
78 1
        $prevADLE = ini_get($adle);
79 1
        !$prevADLE && ini_set($adle, "true");
80 1
        $content = @file($absolutePath, FILE_IGNORE_NEW_LINES);
81 1
        !$prevADLE && ini_set($adle, "false");
82 1
        if (is_bool($content)) {
83
            throw new \Exception(sprintf(self::EXCEPTION_READ_ERROR, $absolutePath));
84
        }
85 1
        $this->content = $content;
86 1
        return $this;
87
    }
88
89
    /**
90
     * Gets the source iterator.
91
     *
92
     * @param string|null $strContent  The string content
93
     *
94
     * @throws \Exception if self::content is empty or splitting on linefeed has failed
95
     * @return \Generator  The source iterator.
96
     */
97 3
    private function getSourceGenerator($strContent = null):\Generator
98
    {
99 3
        if (is_null($strContent) && is_null($this->content)) {
100 1
            throw new \Exception(self::EXCEPTION_LINE_SPLIT);
101
        }
102 2
        if (!is_null($this->content)) {
103
            $source = $this->content;
104
        } else {
105 2
            $simplerLineFeeds = preg_replace('/(\r\n|\r)$/', "\n", (string) $strContent);
106 2
            $source = preg_split("/\n/m", $simplerLineFeeds, 0, \PREG_SPLIT_DELIM_CAPTURE);
107
        }
108 2
        if (!is_array($source) || !count($source)) {
109
            throw new \Exception(self::EXCEPTION_LINE_SPLIT);
110
        }
111 2
        foreach ($source as $key => $value) {
112 2
            yield ++$key => $value;
113
        }
114 2
    }
115
116
    /**
117
     * Parse Yaml lines into a hierarchy of Node
118
     *
119
     * @param string $strContent The Yaml string or null to parse loaded content
120
     *
121
     * @throws \Exception    if content is not available as $strContent or as $this->content (from file)
122
     * @throws \ParseError  if any error during parsing or building
123
     *
124
     * @return array|YamlObject|null      null on errors if NO_PARSING_EXCEPTIONS is set, otherwise an array of YamlObject or just YamlObject
125
     */
126 2
    public function parse($strContent = null)
127
    {
128 2
        $generator = $this->getSourceGenerator($strContent);
129 2
        $previous = $root = new Nodes\Root();
130
        try {
131 2
            foreach ($generator as $lineNB => $lineString) {
132 2
                $node = NodeFactory::get($lineString, $lineNB);
133 2
                if ($this->_debug === 1) echo get_class($node)."\n";
134 2
                if ($this->needsSpecialProcess($node, $previous)) continue;
135 2
                $this->_attachBlankLines($previous);
136 2
                switch ($node->indent <=> $previous->indent) {
137 1
                    case -1: $target = $previous->getTargetOnLessIndent($node);
138 1
                        break;
139 2
                    case 0:  $target = $previous->getTargetOnEqualIndent($node);
140 1
                        break;
141 2
                    default: $target = $previous->getTargetOnMoreIndent($node);
142
                }
143 2
                $previous = $target->add($node);
144
            }
145 2
            $this->_attachBlankLines($previous);
146 2
            return $this->_debug === 1 ? null : Builder::buildContent($root, $this->_debug);
147 1
        } catch (\Throwable $e) {
148 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 131. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
149
        }
150
    }
151
152
153
    /**
154
     * Attach blank (empty) Nodes saved in $_blankBuffer to their parent (it means they are meaningful content)
155
     *
156
     * @param nodes\NodeGeneric  $previous   The previous Node
157
     *
158
     * @return null
159
     */
160 3
    private function _attachBlankLines(Nodes\NodeGeneric $previous)
161
    {
162 3
        foreach ($this->_blankBuffer as $blankNode) {
163 1
            if ($blankNode !== $previous) {
164 1
                $blankNode->getParent()->add($blankNode);
165
            }
166
        }
167 3
        $this->_blankBuffer = [];
168 3
    }
169
170
    /**
171
     * For certain (special) Nodes types some actions are required BEFORE parent assignment
172
     *
173
     * @param Nodes\NodeGeneric   $previous   The previous Node
174
     *
175
     * @return boolean  if True self::parse skips changing previous and adding to parent
176
     * @see self::parse
177
     */
178 3
    private function needsSpecialProcess(Nodes\NodeGeneric $current, Nodes\NodeGeneric $previous):bool
179
    {
180 3
        $deepest = $previous->getDeepestNode();
181 3
        if ($deepest instanceof Nodes\Partial) {
182 1
            return $deepest->specialProcess($current,  $this->_blankBuffer);
183 3
        } elseif(!($current instanceof Nodes\Partial)) {
184 3
            return $current->specialProcess($previous, $this->_blankBuffer);
185
        }
186 1
        return false;
187
    }
188
189
    // private function onError(\Throwable $e, \Generator $generator)
190 2
    private function onError(\Throwable $e, int $lineNB)
191
    {
192 2
        $file = $this->filePath ? realpath($this->filePath) : '#YAML STRING#';
193 2
        $message = $e->getMessage()."\n ".$e->getFile().":".$e->getLine();
194 2
        if ($this->_options & self::NO_PARSING_EXCEPTIONS) {
195 1
            self::$error = $message;
196 1
            return null;
197
        }
198 1
        throw new \Exception($message." for $file:".$lineNB, 1, $e);
199
    }
200
}
201