-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPicoAnnotationParser.php
More file actions
505 lines (463 loc) · 16.1 KB
/
PicoAnnotationParser.php
File metadata and controls
505 lines (463 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
<?php
namespace MagicObject\Util\ClassUtil;
use InvalidArgumentException;
use MagicObject\Exceptions\InvalidAnnotationException;
use MagicObject\Exceptions\InvalidParameterException;
use MagicObject\Exceptions\ZeroArgumentException;
use MagicObject\Util\PicoGenericObject;
use MagicObject\Util\PicoStringUtil;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
/**
* Annotation parser for handling and processing annotations in PHP classes.
*
* This class is designed to read, parse, and manage annotations present in
* the doc comments of classes, methods, and properties. It provides functionalities
* to retrieve annotations as arrays or objects, handle key-value pairs from query
* strings, and validate input parameters.
*
* The `PicoAnnotationParser` is particularly useful in frameworks or libraries
* that rely on annotations for configuration, routing, or metadata purposes.
*
* @author Kamshory
* @package MagicObject\Util\ClassUtil
* @link https://github.com/Planetbiru/MagicObject
*/
class PicoAnnotationParser
{
const METHOD = "method";
const PROPERTY = "property";
/**
* Raw docblock
*
* @var string
*/
private $rawDocBlock;
/**
* Parameters
*
* @var array
*/
private $parameters;
/**
* Key pattern
*
* @var string
*/
private $keyPattern = "[A-z0-9\_\-]+";
/**
* End pattern
*
* @var string
*/
private $endPattern = "[ ]*(?:@|\r\n|\n)";
/**
* Parsed state
*
* @var bool
*/
private $parsedAll = false;
/**
* Reflection object
*
* @var ReflectionClass|ReflectionMethod|ReflectionProperty
*/
private $reflection;
/**
* Constructor
*
* @param mixed ...$args
* @throws ZeroArgumentException|InvalidParameterException
*/
public function __construct()
{
$arguments = func_get_args();
$count = count($arguments);
// get reflection from class or class/method
// (depends on constructor arguments)
if ($count === 0) {
throw new ZeroArgumentException("No zero argument constructor allowed");
} else if ($count === 1) {
$reflection = new ReflectionClass($arguments[0]);
} else {
$type = $count === 3 ? $arguments[2] : self::METHOD;
if ($type === self::METHOD) {
$reflection = new ReflectionMethod($arguments[0], $arguments[1]);
} else if ($type === self::PROPERTY) {
$reflection = new ReflectionProperty($arguments[0], $arguments[1]);
} else {
throw new InvalidParameterException("Invalid type for $type");
}
}
$this->reflection = $reflection;
$this->rawDocBlock = $reflection->getDocComment();
$this->parameters = array();
}
/**
* Retrieves all properties of the reflected class or method.
*
* @return ReflectionProperty[] An array of ReflectionProperty objects.
*/
public function getProperties()
{
return $this->reflection->getProperties();
}
/**
* Checks if the given value is null or empty.
*
* @param string $value The value to check.
* @return bool true if the value is null or empty, otherwise false.
*/
private function isNullOrEmpty($value)
{
return !isset($value) || empty($value);
}
/**
* Parses a single annotation based on the provided key.
*
* @param string $key The annotation key to parse.
* @return array|null The parsed value(s) of the annotation or null if not found.
*/
private function parseSingle($key)
{
$ret = null;
if($this->isNullOrEmpty($key))
{
return array();
}
if (isset($this->parameters[$key])) {
$ret = $this->parameters[$key];
} else {
if (preg_match("/@" . preg_quote($key) . $this->endPattern . "/", $this->rawDocBlock, $match)) {
$ret = true;
} else {
preg_match_all("/@" . preg_quote($key) . "(.*)" . $this->endPattern . "/U", $this->rawDocBlock, $matches);
$size = sizeof($matches[1]);
// not found
if ($size === 0) {
$ret = null;
}
// found one, save as scalar
elseif ($size === 1) {
$ret = $this->parseValue($matches[1][0]);
}
// found many, save as array
else {
$this->parameters[$key] = array();
foreach ($matches[1] as $elem) {
$this->parameters[$key][] = $this->parseValue($elem);
}
$ret = $this->parameters[$key];
}
}
}
return $ret;
}
/**
* Parses all annotations found in the raw docblock.
*
* This method should not be called directly; use `getParameters()` to access
* parsed parameters instead.
*
* @return void
*/
private function parse()
{
$pattern = "/@(?=(.*)" . $this->endPattern . ")/U";
preg_match_all($pattern, $this->rawDocBlock, $matches);
foreach ($matches[1] as $rawParameter) {
if (preg_match("/^(" . $this->keyPattern . ")(.*)$/", $rawParameter, $match)) {
$parsedValue = $this->parseValue($match[2]);
if (isset($this->parameters[$match[1]])) {
$this->parameters[$match[1]] = array_merge((array)$this->parameters[$match[1]], (array)$parsedValue);
} else {
if($parsedValue == null)
{
$this->parameters[$match[1]] = new PicoEmptyParameter();
}
else
{
$this->parameters[$match[1]] = $parsedValue;
}
}
} else if (preg_match("/^" . $this->keyPattern . "$/", $rawParameter, $match)) {
$this->parameters[$rawParameter] = true;
} else {
$this->parameters[$rawParameter] = new PicoEmptyParameter();
}
}
$this->fixDuplication();
}
/**
* Fixes duplicated annotations by keeping only the last occurrence.
*
* This method is called during the parsing process.
*
* @return void
*/
private function fixDuplication()
{
foreach($this->parameters as $key=>$value)
{
if(is_array($value))
{
$end = end($value);
$this->parameters[$key] = $end;
}
}
}
/**
* Retrieves declared variables from a specified annotation.
*
* @param string $name The name of the annotation to retrieve variables from.
* @return string[] An array of declared variables.
*/
public function getVariableDeclarations($name)
{
$declarations = (array)$this->getParameter($name);
foreach ($declarations as &$declaration) {
$declaration = $this->parseVariableDeclaration($declaration, $name);
}
return $declarations;
}
/**
* Parses a variable declaration from an annotation.
*
* @param mixed $declaration The raw declaration string.
* @param string $name The name of the annotation for error context.
* @return string[] An array containing 'type' and 'name'.
* @throws InvalidArgumentException if the declaration is not a string or is empty.
*/
private function parseVariableDeclaration($declaration, $name)
{
$type = gettype($declaration);
if ($type !== 'string') {
throw new InvalidArgumentException(
"Raw declaration must be string, $type given. Key='$name'."
);
}
if (strlen($declaration) === 0) {
throw new InvalidArgumentException(
"Raw declaration cannot have zero length. Key='$name'."
);
}
$declaration = explode(" ", $declaration);
if (sizeof($declaration) == 1) {
// string is default type
array_unshift($declaration, "string");
}
// take first two as type and name
$declaration = array(
'type' => $declaration[0],
'name' => $declaration[1]
);
return $declaration;
}
/**
* Parse value
*
* @param string $originalValue Original value
* @return mixed
*/
private function parseValue($originalValue)
{
if ($originalValue && $originalValue !== 'null') {
// try to json decode, if cannot then store as string
if (($json = json_decode($originalValue, true)) === null) {
$value = $originalValue;
} else {
$value = $json;
}
} else {
$value = null;
}
return $value;
}
/**
* Retrieves all parameters from the parsed annotations.
*
* If the annotations have not been parsed yet, this method will trigger parsing.
*
* @return array An associative array of parsed annotation parameters.
*/
public function getParameters()
{
if (!$this->parsedAll) {
$this->parse();
$this->parsedAll = true;
}
return $this->parameters;
}
/**
* Retrieves all parameters as an object of type PicoGenericObject.
*
* If the annotations have not been parsed yet, this method will trigger parsing.
*
* @return PicoGenericObject An object containing the parsed annotation parameters.
*/
public function getParametersAsObject()
{
if (!$this->parsedAll) {
$this->parse();
$this->parsedAll = true;
}
return new PicoGenericObject($this->parameters);
}
/**
* Retrieves a specific parameter by its key.
*
* @param string $key The key of the parameter to retrieve.
* @return mixed The value of the specified parameter or null if not found.
*/
public function getParameter($key)
{
return $this->parseSingle($key);
}
/**
* Get the first parameter for a given key from the parsed annotations.
*
* This method retrieves the first value associated with the specified key.
* If the parameter does not exist or is null, it returns null.
* If the parameter is an array, it returns the first string element.
* Otherwise, it returns the value directly.
*
* @param string $key The key for which to retrieve the first parameter.
* @return string|null The first parameter value, or null if not found.
*/
public function getFirstParameter($key)
{
$parameters = $this->parseSingle($key);
if($parameters == null)
{
return null;
}
if(is_array($parameters) && is_string($parameters[0]))
{
return $parameters[0];
}
else
{
return $parameters;
}
}
/**
* Combine and merge two arrays, where the first array contains keys and the second contains values.
*
* This method checks if both arrays are set and are of the correct type.
* It combines them into a new associative array and returns the merged result.
*
* @param array $matches An array of matched keys and values.
* @param array $pair An associative array to merge with.
* @return array The merged array containing keys and values from both input arrays.
*/
private function combineAndMerge($matches, $pair)
{
if(isset($matches[1]) && isset($matches[2]) && is_array($matches[1]) && is_array($matches[2]))
{
$pair2 = array_combine($matches[1], $matches[2]);
// merge $pair and $pair2 into $pair3
return array_merge($pair, $pair2);
}
else
{
return $pair;
}
}
/**
* Parse key-value pairs from parameters string.
*
* This method extracts key-value pairs from parameters string, which may contain
* attributes with or without quotes. Numeric attributes will have an underscore
* prefix. Throws an exception if the input is invalid.
*
* @param string $parametersString The parameters string to parse.
* @return string[] An associative array of parsed key-value pairs.
* @throws InvalidAnnotationException If the annotations are invalid or cannot be parsed.
*/
public function parseKeyValue($parametersString)
{
if(!isset($parametersString) || empty($parametersString) || $parametersString instanceof PicoEmptyParameter)
{
return array();
}
if(!is_string($parametersString))
{
throw new InvalidAnnotationException("Invalid parameters string");
}
// For every modification, please test regular expression with https://regex101.com/
// parse attributes with quotes
$pattern1 = '/([_\-\w+]+)\=\"([a-zA-Z0-9\-\+ _,.\(\)\{\}\`\~\!\@\#\$\%\^\*\\\|\<\>\[\]\/&%?=:;\'\t\r\n|\r|\n]+)\"/m'; // NOSONAR
preg_match_all($pattern1, $parametersString, $matches1);
$pair1 = array_combine($matches1[1], $matches1[2]);
// parse attributes without quotes
$pattern2 = '/([_\-\w+]+)\=([a-zA-Z0-9._]+)/m'; // NOSONAR
preg_match_all($pattern2, $parametersString, $matches2);
$pair3 = $this->combineAndMerge($matches2, $pair1);
// parse attributes without any value
// 🔴 FIX: remove quoted values before parsing boolean attributes
$cleaned = preg_replace($pattern1, '', $parametersString);
// parse attributes without any value (boolean flags)
$pattern3 = '/\b([A-Za-z_][A-Za-z0-9_\-]*)\b/m'; // NOSONAR
preg_match_all($pattern3, $cleaned, $matches3);
$pair4 = array();
if(isset($matches3) && isset($matches3[0]) && is_array($matches3[0]))
{
$keys = array_keys($pair3);
foreach($matches3[0] as $val)
{
if($this->matchArgs($keys, $val))
{
if(is_numeric($val))
{
// prepend attribute with underscore due unexpected array key
$pair4["_".$val] = true;
}
else
{
$pair4[$val] = true;
}
}
}
}
// merge $pair3 and $pair4 into result
return array_merge($pair3, $pair4);
}
/**
* Check if the provided value matches the expected criteria.
*
* This method checks if the given value does not contain an equals sign, quotes,
* and is not present in the provided keys array.
*
* @param array $keys The array of valid keys.
* @param string $val The value to check.
* @return bool true if the value matches the criteria, otherwise false.
*/
private function matchArgs($keys, $val)
{
return stripos($val, '=') === false && stripos($val, '"') === false && stripos($val, "'") === false && !in_array($val, $keys);
}
/**
* Parse parameters from parameters string and return them as a PicoGenericObject.
*
* This method transforms the key-value pairs parsed from the parameters string
* into an instance of PicoGenericObject. All numeric attributes will be
* prefixed with an underscore.
*
* @param string $parametersString The parameters string to parse.
* @return PicoGenericObject An object containing the parsed key-value pairs.
* @throws InvalidAnnotationException If the annotations are invalid or cannot be parsed.
*/
public function parseKeyValueAsObject($parametersString)
{
if(PicoStringUtil::isNullOrEmpty($parametersString))
{
return new PicoGenericObject();
}
if(!is_string($parametersString))
{
throw new InvalidAnnotationException("Invalid parameters string");
}
return new PicoGenericObject($this->parseKeyValue($parametersString));
}
}