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 (!count($source)) { |
|
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 | 1 | -1 => $previous->getTargetOnLessIndent($node), |
|
134 | 1 | 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 | if($current instanceof Nodes\Comment) { |
||
0 ignored issues
–
show
|
|||
180 | return true; |
||
181 | } |
||
182 | } |
||
183 | 1 | return false; |
|
184 | } |
||
185 | |||
186 | 2 | private function onError(\Throwable $e) |
|
187 | { |
||
188 | 2 | $file = $this->filePath ? realpath($this->filePath) : '#YAML STRING#'; |
|
189 | 2 | $message = $e->getMessage() . "\n " . $e->getFile() . ":" . $e->getLine(); |
|
190 | 2 | if ($this->_options & self::NO_PARSING_EXCEPTIONS) { |
|
191 | 1 | self::$error = $message; |
|
192 | 1 | return null; |
|
193 | } |
||
194 | 1 | throw new \Exception($message . " for $file:" . $e->getLine(), 1, $e); |
|
195 | } |
||
196 | } |
||
197 |
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.
Unreachable code is most often the result of
return
,die
orexit
statements that have been added for debug purposes.In the above example, the last
return false
will never be executed, because a return statement has already been met in every possible execution path.