Test Failed
Branch master (bee4a6)
by stéphane
14:37
created

Loader::getSourceGenerator()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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