1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
namespace Dallgoot\Yaml; |
5
|
|
|
|
6
|
|
|
/** |
7
|
|
|
* TODO |
8
|
|
|
* |
9
|
|
|
* @author Stéphane Rebai <[email protected]> |
10
|
|
|
* @license Apache 2.0 |
11
|
|
|
* @link TODO : url to specific online doc |
12
|
|
|
*/ |
13
|
|
|
final class Loader |
14
|
|
|
{ |
15
|
|
|
//public |
16
|
|
|
/* @var null|string */ |
17
|
|
|
public static $error; |
18
|
|
|
public const IGNORE_DIRECTIVES = 1;//DONT include_directive |
19
|
|
|
public const IGNORE_COMMENTS = 2;//DONT include_comments |
20
|
|
|
public const NO_PARSING_EXCEPTIONS = 4;//DONT throw Exception on parsing errors |
21
|
|
|
public const NO_OBJECT_FOR_DATE = 8;//DONT import date strings as dateTime Object |
22
|
|
|
|
23
|
|
|
//private |
24
|
|
|
/* @var null|false|array */ |
25
|
|
|
private $content; |
26
|
|
|
/* @var null|string */ |
27
|
|
|
private $filePath; |
28
|
|
|
/* @var integer */ |
29
|
|
|
private $_debug = 0; |
30
|
|
|
/* @var integer */ |
31
|
|
|
private $_options = 0; |
32
|
|
|
/* @var array */ |
33
|
|
|
private $_blankBuffer = []; |
34
|
|
|
|
35
|
|
|
//Exceptions messages |
36
|
|
|
private const INVALID_VALUE = self::class.": at line %d"; |
37
|
|
|
private const EXCEPTION_NO_FILE = self::class.": file '%s' does not exists (or path is incorrect?)"; |
38
|
|
|
private const EXCEPTION_READ_ERROR = self::class.": file '%s' failed to be loaded (permission denied ?)"; |
39
|
|
|
private const EXCEPTION_LINE_SPLIT = self::class.": content is not a string (maybe a file error?)"; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Loader constructor |
43
|
|
|
* |
44
|
|
|
* @param string|null $absolutePath The file absolute path |
45
|
|
|
* @param int|null $options The options (bitmask as int value) |
46
|
|
|
* @param integer|bool|null $debug The debug level as either boolean (true=1) or any integer |
47
|
|
|
*/ |
48
|
11 |
|
public function __construct($absolutePath = null, $options = null, $debug = 0) |
49
|
|
|
{ |
50
|
11 |
|
$this->_debug = is_null($debug) ? 0 : min($debug, 3); |
51
|
11 |
|
$this->_options = is_int($options) ? $options : $this->_options; |
52
|
11 |
|
if (is_string($absolutePath)) { |
53
|
1 |
|
$this->load($absolutePath); |
54
|
|
|
} |
55
|
11 |
|
} |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Load a file and save its content as $content |
59
|
|
|
* |
60
|
|
|
* @param string $absolutePath The absolute path of a file |
61
|
|
|
* |
62
|
|
|
* @throws \Exception if file don't exist OR reading failed |
63
|
|
|
* |
64
|
|
|
* @return self ( returns the same Loader ) |
65
|
|
|
*/ |
66
|
3 |
|
public function load(string $absolutePath):Loader |
67
|
|
|
{ |
68
|
3 |
|
if (!file_exists($absolutePath)) { |
69
|
1 |
|
throw new \Exception(sprintf(self::EXCEPTION_NO_FILE, $absolutePath)); |
70
|
|
|
} |
71
|
2 |
|
$this->filePath = $absolutePath; |
72
|
2 |
|
$adle = "auto_detect_line_endings"; |
73
|
2 |
|
$prevADLE = ini_get($adle); |
74
|
2 |
|
!$prevADLE && ini_set($adle, "true"); |
75
|
2 |
|
$content = @file($absolutePath, FILE_IGNORE_NEW_LINES); |
76
|
2 |
|
!$prevADLE && ini_set($adle, "false"); |
77
|
2 |
|
if (is_bool($content)) { |
78
|
1 |
|
throw new \Exception(sprintf(self::EXCEPTION_READ_ERROR, $absolutePath)); |
79
|
|
|
} |
80
|
1 |
|
$this->content = $content; |
81
|
1 |
|
return $this; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Gets the source iterator. |
86
|
|
|
* |
87
|
|
|
* @param string|null $strContent The string content |
88
|
|
|
* |
89
|
|
|
* @throws \Exception if self::content is empty or splitting on linefeed has failed |
90
|
|
|
* @return \Generator The source iterator. |
91
|
|
|
*/ |
92
|
3 |
|
private function getSourceGenerator($strContent = null):\Generator |
93
|
|
|
{ |
94
|
3 |
|
if (is_null($strContent) && is_null($this->content)) { |
95
|
1 |
|
throw new \Exception(self::EXCEPTION_LINE_SPLIT); |
96
|
|
|
} |
97
|
2 |
|
if (!is_null($this->content)) { |
98
|
|
|
$source = $this->content; |
|
|
|
|
99
|
|
|
} else { //TODO : be more permissive on $strContent values |
100
|
2 |
|
$simplerLineFeeds = preg_replace('/(\r\n|\r)/', "\n", $strContent); |
101
|
2 |
|
$source = preg_split("/\n/m", $simplerLineFeeds, 0, PREG_SPLIT_DELIM_CAPTURE); |
102
|
|
|
} |
103
|
2 |
|
if (!is_array($source) || !count($source)) { |
104
|
|
|
throw new \Exception(self::EXCEPTION_LINE_SPLIT); |
105
|
|
|
} |
106
|
2 |
|
foreach ($source as $key => $value) { |
107
|
2 |
|
yield ++$key => $value; |
108
|
|
|
} |
109
|
2 |
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Parse Yaml lines into a hierarchy of Node |
113
|
|
|
* |
114
|
|
|
* @param string $strContent The Yaml string or null to parse loaded content |
115
|
|
|
* |
116
|
|
|
* @throws \Exception if content is not available as $strContent or as $this->content (from file) |
117
|
|
|
* @throws \ParseError if any error during parsing or building |
118
|
|
|
* |
119
|
|
|
* @return array|YamlObject|null null on errors if NO_PARSING_EXCEPTIONS is set, otherwise an array of YamlObject or just YamlObject |
120
|
|
|
*/ |
121
|
2 |
|
public function parse($strContent = null) |
122
|
|
|
{ |
123
|
2 |
|
$generator = $this->getSourceGenerator($strContent); |
124
|
2 |
|
$previous = $root = new NodeRoot(); |
125
|
|
|
try { |
126
|
2 |
|
foreach ($generator as $lineNb => $lineString) { |
127
|
2 |
|
$node = NodeFactory::get($lineString, $lineNb); |
128
|
2 |
|
if ($this->_debug === 1) echo get_class($node)."\n"; |
129
|
2 |
|
if ($this->needsSpecialProcess($node, $previous)) continue; |
130
|
2 |
|
$this->attachBlankLines($previous); |
131
|
2 |
|
switch ($node->indent <=> $previous->indent) { |
132
|
1 |
|
case -1: $target = $previous->getTargetOnLessIndent($node); |
133
|
1 |
|
break; |
134
|
2 |
|
case 0: $target = $previous->getTargetOnEqualIndent($node); |
135
|
1 |
|
break; |
136
|
2 |
|
default: $target = $previous->getTargetOnMoreIndent($node); |
137
|
|
|
} |
138
|
2 |
|
$previous = $target->add($node); |
139
|
|
|
} |
140
|
2 |
|
$this->attachBlankLines($previous); |
141
|
2 |
|
if ($this->_debug === 1){ |
142
|
|
|
return; |
143
|
|
|
} |
144
|
2 |
|
return Builder::buildContent($root, $this->_debug); |
145
|
1 |
|
} catch (\Error|\Exception|\ParseError $e) { |
146
|
1 |
|
$this->onError($e, $generator); |
147
|
|
|
} |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* Attach blank(empty) Nodes savec in $blankBuffer to their parent (it means they are needed) |
153
|
|
|
* |
154
|
|
|
* @param array $emptyLines The empty lines |
155
|
|
|
* @param Node $previous The previous |
156
|
|
|
* |
157
|
|
|
* @return null |
158
|
|
|
*/ |
159
|
3 |
|
public function attachBlankLines(Node &$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 Node $previous The previous Node |
173
|
|
|
* @param array $emptyLines The empty lines |
174
|
|
|
* |
175
|
|
|
* @return boolean if True self::parse skips changing previous and adding to parent |
176
|
|
|
* @see self::parse |
177
|
|
|
*/ |
178
|
3 |
|
public function needsSpecialProcess(Node $current, Node &$previous):bool |
179
|
|
|
{ |
180
|
3 |
|
$deepest = $previous->getDeepestNode(); |
181
|
3 |
|
if ($deepest instanceof NodePartial) { |
182
|
1 |
|
return $deepest->specialProcess($current, $this->_blankBuffer); |
183
|
3 |
|
} elseif(!($current instanceof NodePartial)) { |
184
|
3 |
|
return $current->specialProcess($previous, $this->_blankBuffer); |
185
|
|
|
} |
186
|
1 |
|
return false; |
187
|
|
|
} |
188
|
|
|
|
189
|
3 |
|
public function onError(object $e, \Generator $generator) |
190
|
|
|
{ |
191
|
3 |
|
$file = $this->filePath ? realpath($this->filePath) : '#YAML STRING#'; |
192
|
3 |
|
$message = $e->getMessage()."\n ".$e->getFile().":".$e->getLine(); |
193
|
3 |
|
if ($this->_options & self::NO_PARSING_EXCEPTIONS) { |
194
|
1 |
|
self::$error = $message; |
195
|
1 |
|
return null; |
196
|
|
|
} |
197
|
2 |
|
$line = $generator->key() ?? 'X'; |
198
|
2 |
|
throw new \Exception($message." for $file:".$line, 1, $e); |
199
|
|
|
} |
200
|
|
|
} |
201
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.