Passed
Push — master ( ea208f...e8c340 )
by stéphane
02:39
created

Loader   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 171
Duplicated Lines 0 %

Test Coverage

Coverage 95.38%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 27
eloc 75
c 1
b 0
f 0
dl 0
loc 171
ccs 62
cts 65
cp 0.9538
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 4
A onError() 0 9 3
A parse() 0 24 5
A _attachBlankLines() 0 8 3
A load() 0 14 3
A getSourceGenerator() 0 18 6
A needsSpecialProcess() 0 9 3
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 11
    public function __construct(?string $absolutePath = null, ?int $options = null, ?int $debug = 0)
47
    {
48 11
        $this->_debug   = is_null($debug) ? 0 : min($debug, 3);
49 11
        $this->_options = is_int($options) ? $options : $this->_options;
50 11
        if (is_string($absolutePath)) {
51 1
            $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 3
    public function load(string $absolutePath): Loader
65
    {
66 3
        if (!file_exists($absolutePath)) {
67 2
            throw new \Exception(sprintf(self::EXCEPTION_NO_FILE, $absolutePath));
68
        }
69 1
        $this->filePath = $absolutePath;
70
71 1
        $content = @file($absolutePath, FILE_IGNORE_NEW_LINES);
72
73 1
        if (is_bool($content)) {
74
            throw new \Exception(sprintf(self::EXCEPTION_READ_ERROR, $absolutePath));
75
        }
76 1
        $this->content = \SplFixedArray::fromArray($content, false);
77 1
        return $this;
78
    }
79
80
    /**
81
     * Gets the source iterator.
82
     *
83
     * @param string|null $strContent  The string content
84
     *
85
     * @throws \Exception if self::content is empty or splitting on linefeed has failed
86
     * @return \Generator  The source iterator.
87
     */
88 3
    private function getSourceGenerator(?string $strContent = null): \Generator
89
    {
90 3
        if (is_null($strContent)) {
91 1
            if(is_null($this->content)) {
92 1
                throw new \Exception(self::EXCEPTION_LINE_SPLIT);
93
            }else {
94
                $source = $this->content;
95
            }
96
        } else {
97 2
            $simplerLineFeeds = preg_replace('/(\r\n|\r)$/', "\n", (string) $strContent);
98 2
            $source = preg_split("/\n/m", $simplerLineFeeds, 0, \PREG_SPLIT_DELIM_CAPTURE);
99 2
            if (!is_array($source) || !count($source)) {
0 ignored issues
show
introduced by
The condition is_array($source) is always true.
Loading history...
100
                throw new \Exception(self::EXCEPTION_LINE_SPLIT);
101
            }
102 2
            $source = \SplFixedArray::fromArray($source, false);
103
        }
104 2
        foreach ($source as $key => $value) {
105 2
            yield ++$key => $value;
106
        }
107
    }
108
109
    /**
110
     * Parse Yaml lines into a hierarchy of Node
111
     *
112
     * @param ?string $strContent The Yaml string or null to parse loaded content
113
     *
114
     * @throws \Exception    if content is not available as $strContent or as $this->content (from file)
115
     * @throws \ParseError  if any error during parsing or building
116
     *
117
     * @return array|YamlObject|null      null on errors if NO_PARSING_EXCEPTIONS is set, otherwise an array of YamlObject or just YamlObject
118
     */
119 2
    public function parse(?string $strContent = null)
120
    {
121 2
        if(!is_null($strContent)) {
122 2
            $this->content = null;
123
        }
124 2
        $generator = $this->getSourceGenerator($strContent);
125 2
        $previous = $root = new Nodes\Root();
126 2
        $debugNodeFactory = $this->_debug === 1;
127
        try {
128 2
            foreach ($generator as $lineNB => $lineString) {
129 2
                $node = NodeFactory::get($lineString, $lineNB, $debugNodeFactory);
130 2
                if ($this->needsSpecialProcess($node, $previous)) continue;
131 2
                $this->_attachBlankLines($previous);
132 2
                $target = match ($node->indent <=> $previous->indent) {
133 2
                    -1 => $previous->getTargetOnLessIndent($node),
134 2
                    0  => $previous->getTargetOnEqualIndent($node),
135 2
                    1  => $previous->getTargetOnMoreIndent($node)
136 2
                };
137 2
                $previous = $target->add($node);
138
            }
139 2
            $this->_attachBlankLines($previous);
140 2
            return (new Builder($this->_options, $this->_debug))->buildContent($root);
141 1
        } catch (\Throwable $e) {
142 1
            $this->onError($e);
143
        }
144
    }
145
146
147
    /**
148
     * Attach blank (empty) Nodes saved in $_blankBuffer to their parent (it means they are meaningful content)
149
     *
150
     * @param NodeGeneric  $previous   The previous Node
151
     *
152
     * @return null
153
     */
154 3
    private function _attachBlankLines(NodeGeneric $previous)
155
    {
156 3
        foreach ($this->_blankBuffer as $blankNode) {
157 1
            if ($blankNode !== $previous) {
158 1
                $blankNode->getParent()->add($blankNode);
159
            }
160
        }
161 3
        $this->_blankBuffer = [];
162
    }
163
164
    /**
165
     * For certain (special) Nodes types some actions are required BEFORE parent assignment
166
     *
167
     * @param NodeGeneric   $previous   The previous Node
168
     *
169
     * @return boolean  if True self::parse skips changing previous and adding to parent
170
     * @see self::parse
171
     */
172 3
    private function needsSpecialProcess(NodeGeneric $current, NodeGeneric $previous): bool
173
    {
174 3
        $deepest = $previous->getDeepestNode();
175 3
        if ($deepest instanceof Nodes\Partial) {
176 1
            return $deepest->specialProcess($current,  $this->_blankBuffer);
177 3
        } elseif (!($current instanceof Nodes\Partial)) {
178 3
            return $current->specialProcess($previous, $this->_blankBuffer);
179
        }
180 1
        return false;
181
    }
182
183 2
    private function onError(\Throwable $e)
184
    {
185 2
        $file = $this->filePath ? realpath($this->filePath) : '#YAML STRING#';
186 2
        $message = $e->getMessage() . "\n " . $e->getFile() . ":" . $e->getLine();
187 2
        if ($this->_options & self::NO_PARSING_EXCEPTIONS) {
188 1
            self::$error = $message;
189 1
            return null;
190
        }
191 1
        throw new \Exception($message . " for $file:" . $e->getLine(), 1, $e);
192
    }
193
}
194