|
1 <?php |
|
2 /* |
|
3 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|
4 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
5 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|
6 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
|
7 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
8 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
9 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
|
10 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
|
11 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
12 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|
13 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
14 * |
|
15 * This software consists of voluntary contributions made by many individuals |
|
16 * and is licensed under the LGPL. For more information, see |
|
17 * <http://www.doctrine-project.org>. |
|
18 */ |
|
19 |
|
20 namespace Doctrine\Common\Annotations; |
|
21 |
|
22 use Closure; |
|
23 use ReflectionClass; |
|
24 |
|
25 /** |
|
26 * A parser for docblock annotations. |
|
27 * |
|
28 * It is strongly discouraged to change the default annotation parsing process. |
|
29 * |
|
30 * @author Benjamin Eberlei <kontakt@beberlei.de> |
|
31 * @author Guilherme Blanco <guilhermeblanco@hotmail.com> |
|
32 * @author Jonathan Wage <jonwage@gmail.com> |
|
33 * @author Roman Borschel <roman@code-factory.org> |
|
34 * @author Johannes M. Schmitt <schmittjoh@gmail.com> |
|
35 */ |
|
36 final class DocParser |
|
37 { |
|
38 /** |
|
39 * An array of all valid tokens for a class name. |
|
40 * |
|
41 * @var array |
|
42 */ |
|
43 private static $classIdentifiers = array(DocLexer::T_IDENTIFIER, DocLexer::T_TRUE, DocLexer::T_FALSE, DocLexer::T_NULL); |
|
44 |
|
45 /** |
|
46 * The lexer. |
|
47 * |
|
48 * @var Doctrine\Common\Annotations\DocLexer |
|
49 */ |
|
50 private $lexer; |
|
51 |
|
52 /** |
|
53 * Flag to control if the current annotation is nested or not. |
|
54 * |
|
55 * @var boolean |
|
56 */ |
|
57 private $isNestedAnnotation = false; |
|
58 |
|
59 /** |
|
60 * Hashmap containing all use-statements that are to be used when parsing |
|
61 * the given doc block. |
|
62 * |
|
63 * @var array |
|
64 */ |
|
65 private $imports = array(); |
|
66 |
|
67 /** |
|
68 * This hashmap is used internally to cache results of class_exists() |
|
69 * look-ups. |
|
70 * |
|
71 * @var array |
|
72 */ |
|
73 private $classExists = array(); |
|
74 |
|
75 /** |
|
76 * |
|
77 * @var This hashmap is used internally to cache if a class is an annotation or not. |
|
78 * |
|
79 * @var array |
|
80 */ |
|
81 private $isAnnotation = array(); |
|
82 |
|
83 /** |
|
84 * Whether annotations that have not been imported should be ignored. |
|
85 * |
|
86 * @var boolean |
|
87 */ |
|
88 private $ignoreNotImportedAnnotations = false; |
|
89 |
|
90 /** |
|
91 * A list with annotations that are not causing exceptions when not resolved to an annotation class. |
|
92 * |
|
93 * The names must be the raw names as used in the class, not the fully qualified |
|
94 * class names. |
|
95 * |
|
96 * @var array |
|
97 */ |
|
98 private $ignoredAnnotationNames = array(); |
|
99 |
|
100 /** |
|
101 * @var array |
|
102 */ |
|
103 private $namespaceAliases = array(); |
|
104 |
|
105 /** |
|
106 * @var string |
|
107 */ |
|
108 private $context = ''; |
|
109 |
|
110 /** |
|
111 * @var Closure |
|
112 */ |
|
113 private $creationFn = null; |
|
114 |
|
115 /** |
|
116 * Constructs a new DocParser. |
|
117 */ |
|
118 public function __construct() |
|
119 { |
|
120 $this->lexer = new DocLexer; |
|
121 } |
|
122 |
|
123 /** |
|
124 * Sets the annotation names that are ignored during the parsing process. |
|
125 * |
|
126 * The names are supposed to be the raw names as used in the class, not the |
|
127 * fully qualified class names. |
|
128 * |
|
129 * @param array $names |
|
130 */ |
|
131 public function setIgnoredAnnotationNames(array $names) |
|
132 { |
|
133 $this->ignoredAnnotationNames = $names; |
|
134 } |
|
135 |
|
136 /** |
|
137 * @deprecated Will be removed in 3.0 |
|
138 * @param \Closure $func |
|
139 */ |
|
140 public function setAnnotationCreationFunction(\Closure $func) |
|
141 { |
|
142 $this->creationFn = $func; |
|
143 } |
|
144 |
|
145 public function setImports(array $imports) |
|
146 { |
|
147 $this->imports = $imports; |
|
148 } |
|
149 |
|
150 public function setIgnoreNotImportedAnnotations($bool) |
|
151 { |
|
152 $this->ignoreNotImportedAnnotations = (Boolean) $bool; |
|
153 } |
|
154 |
|
155 public function setAnnotationNamespaceAlias($namespace, $alias) |
|
156 { |
|
157 $this->namespaceAliases[$alias] = $namespace; |
|
158 } |
|
159 |
|
160 /** |
|
161 * Parses the given docblock string for annotations. |
|
162 * |
|
163 * @param string $input The docblock string to parse. |
|
164 * @param string $context The parsing context. |
|
165 * @return array Array of annotations. If no annotations are found, an empty array is returned. |
|
166 */ |
|
167 public function parse($input, $context = '') |
|
168 { |
|
169 if (false === $pos = strpos($input, '@')) { |
|
170 return array(); |
|
171 } |
|
172 |
|
173 // also parse whatever character is before the @ |
|
174 if ($pos > 0) { |
|
175 $pos -= 1; |
|
176 } |
|
177 |
|
178 $this->context = $context; |
|
179 $this->lexer->setInput(trim(substr($input, $pos), '* /')); |
|
180 $this->lexer->moveNext(); |
|
181 |
|
182 return $this->Annotations(); |
|
183 } |
|
184 |
|
185 /** |
|
186 * Attempts to match the given token with the current lookahead token. |
|
187 * If they match, updates the lookahead token; otherwise raises a syntax error. |
|
188 * |
|
189 * @param int Token type. |
|
190 * @return bool True if tokens match; false otherwise. |
|
191 */ |
|
192 private function match($token) |
|
193 { |
|
194 if ( ! $this->lexer->isNextToken($token) ) { |
|
195 $this->syntaxError($this->lexer->getLiteral($token)); |
|
196 } |
|
197 |
|
198 return $this->lexer->moveNext(); |
|
199 } |
|
200 |
|
201 /** |
|
202 * Attempts to match the current lookahead token with any of the given tokens. |
|
203 * |
|
204 * If any of them matches, this method updates the lookahead token; otherwise |
|
205 * a syntax error is raised. |
|
206 * |
|
207 * @param array $tokens |
|
208 * @return bool |
|
209 */ |
|
210 private function matchAny(array $tokens) |
|
211 { |
|
212 if ( ! $this->lexer->isNextTokenAny($tokens)) { |
|
213 $this->syntaxError(implode(' or ', array_map(array($this->lexer, 'getLiteral'), $tokens))); |
|
214 } |
|
215 |
|
216 return $this->lexer->moveNext(); |
|
217 } |
|
218 |
|
219 /** |
|
220 * Generates a new syntax error. |
|
221 * |
|
222 * @param string $expected Expected string. |
|
223 * @param array $token Optional token. |
|
224 * @throws SyntaxException |
|
225 */ |
|
226 private function syntaxError($expected, $token = null) |
|
227 { |
|
228 if ($token === null) { |
|
229 $token = $this->lexer->lookahead; |
|
230 } |
|
231 |
|
232 $message = "Expected {$expected}, got "; |
|
233 |
|
234 if ($this->lexer->lookahead === null) { |
|
235 $message .= 'end of string'; |
|
236 } else { |
|
237 $message .= "'{$token['value']}' at position {$token['position']}"; |
|
238 } |
|
239 |
|
240 if (strlen($this->context)) { |
|
241 $message .= ' in ' . $this->context; |
|
242 } |
|
243 |
|
244 $message .= '.'; |
|
245 |
|
246 throw AnnotationException::syntaxError($message); |
|
247 } |
|
248 |
|
249 /** |
|
250 * Attempt to check if a class exists or not. This never goes through the PHP autoloading mechanism |
|
251 * but uses the {@link AnnotationRegistry} to load classes. |
|
252 * |
|
253 * @param string $fqcn |
|
254 * @return boolean |
|
255 */ |
|
256 private function classExists($fqcn) |
|
257 { |
|
258 if (isset($this->classExists[$fqcn])) { |
|
259 return $this->classExists[$fqcn]; |
|
260 } |
|
261 |
|
262 // first check if the class already exists, maybe loaded through another AnnotationReader |
|
263 if (class_exists($fqcn, false)) { |
|
264 return $this->classExists[$fqcn] = true; |
|
265 } |
|
266 |
|
267 // final check, does this class exist? |
|
268 return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn); |
|
269 } |
|
270 |
|
271 /** |
|
272 * Annotations ::= Annotation {[ "*" ]* [Annotation]}* |
|
273 * |
|
274 * @return array |
|
275 */ |
|
276 private function Annotations() |
|
277 { |
|
278 $annotations = array(); |
|
279 |
|
280 while (null !== $this->lexer->lookahead) { |
|
281 if (DocLexer::T_AT !== $this->lexer->lookahead['type']) { |
|
282 $this->lexer->moveNext(); |
|
283 continue; |
|
284 } |
|
285 |
|
286 // make sure the @ is preceded by non-catchable pattern |
|
287 if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) { |
|
288 $this->lexer->moveNext(); |
|
289 continue; |
|
290 } |
|
291 |
|
292 // make sure the @ is followed by either a namespace separator, or |
|
293 // an identifier token |
|
294 if ((null === $peek = $this->lexer->glimpse()) |
|
295 || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true)) |
|
296 || $peek['position'] !== $this->lexer->lookahead['position'] + 1) { |
|
297 $this->lexer->moveNext(); |
|
298 continue; |
|
299 } |
|
300 |
|
301 $this->isNestedAnnotation = false; |
|
302 if (false !== $annot = $this->Annotation()) { |
|
303 $annotations[] = $annot; |
|
304 } |
|
305 } |
|
306 |
|
307 return $annotations; |
|
308 } |
|
309 |
|
310 /** |
|
311 * Annotation ::= "@" AnnotationName ["(" [Values] ")"] |
|
312 * AnnotationName ::= QualifiedName | SimpleName |
|
313 * QualifiedName ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName |
|
314 * NameSpacePart ::= identifier | null | false | true |
|
315 * SimpleName ::= identifier | null | false | true |
|
316 * |
|
317 * @return mixed False if it is not a valid annotation. |
|
318 */ |
|
319 private function Annotation() |
|
320 { |
|
321 $this->match(DocLexer::T_AT); |
|
322 |
|
323 // check if we have an annotation |
|
324 if ($this->lexer->isNextTokenAny(self::$classIdentifiers)) { |
|
325 $this->lexer->moveNext(); |
|
326 $name = $this->lexer->token['value']; |
|
327 } else if ($this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) { |
|
328 $name = ''; |
|
329 } else { |
|
330 $this->syntaxError('namespace separator or identifier'); |
|
331 } |
|
332 |
|
333 while ($this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value']) && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) { |
|
334 $this->match(DocLexer::T_NAMESPACE_SEPARATOR); |
|
335 $this->matchAny(self::$classIdentifiers); |
|
336 $name .= '\\'.$this->lexer->token['value']; |
|
337 } |
|
338 |
|
339 if (strpos($name, ":") !== false) { |
|
340 list ($alias, $name) = explode(':', $name); |
|
341 // If the namespace alias doesnt exist, skip until next annotation |
|
342 if ( ! isset($this->namespaceAliases[$alias])) { |
|
343 $this->lexer->skipUntil(DocLexer::T_AT); |
|
344 return false; |
|
345 } |
|
346 $name = $this->namespaceAliases[$alias] . $name; |
|
347 } |
|
348 |
|
349 // only process names which are not fully qualified, yet |
|
350 if ('\\' !== $name[0] && !$this->classExists($name)) { |
|
351 $alias = (false === $pos = strpos($name, '\\'))? $name : substr($name, 0, $pos); |
|
352 |
|
353 if (isset($this->imports[$loweredAlias = strtolower($alias)])) { |
|
354 if (false !== $pos) { |
|
355 $name = $this->imports[$loweredAlias].substr($name, $pos); |
|
356 } else { |
|
357 $name = $this->imports[$loweredAlias]; |
|
358 } |
|
359 } elseif (isset($this->imports['__DEFAULT__']) && $this->classExists($this->imports['__DEFAULT__'].$name)) { |
|
360 $name = $this->imports['__DEFAULT__'].$name; |
|
361 } elseif (isset($this->imports['__NAMESPACE__']) && $this->classExists($this->imports['__NAMESPACE__'].'\\'.$name)) { |
|
362 $name = $this->imports['__NAMESPACE__'].'\\'.$name; |
|
363 } else { |
|
364 if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) { |
|
365 return false; |
|
366 } |
|
367 |
|
368 throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s was never imported.', $name, $this->context)); |
|
369 } |
|
370 } |
|
371 |
|
372 if (!$this->classExists($name)) { |
|
373 throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context)); |
|
374 } |
|
375 |
|
376 if (!$this->isAnnotation($name)) { |
|
377 return false; |
|
378 } |
|
379 |
|
380 // Verifies that the annotation class extends any class that contains "Annotation". |
|
381 // This is done to avoid coupling of Doctrine Annotations against other libraries. |
|
382 |
|
383 |
|
384 // at this point, $name contains the fully qualified class name of the |
|
385 // annotation, and it is also guaranteed that this class exists, and |
|
386 // that it is loaded |
|
387 |
|
388 // Next will be nested |
|
389 $this->isNestedAnnotation = true; |
|
390 |
|
391 $values = array(); |
|
392 if ($this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) { |
|
393 $this->match(DocLexer::T_OPEN_PARENTHESIS); |
|
394 |
|
395 if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) { |
|
396 $values = $this->Values(); |
|
397 } |
|
398 |
|
399 $this->match(DocLexer::T_CLOSE_PARENTHESIS); |
|
400 } |
|
401 |
|
402 return $this->newAnnotation($name, $values); |
|
403 } |
|
404 |
|
405 /** |
|
406 * Verify that the found class is actually an annotation. |
|
407 * |
|
408 * This can be detected through two mechanisms: |
|
409 * 1. Class extends Doctrine\Common\Annotations\Annotation |
|
410 * 2. The class level docblock contains the string "@Annotation" |
|
411 * |
|
412 * @param string $name |
|
413 * @return bool |
|
414 */ |
|
415 private function isAnnotation($name) |
|
416 { |
|
417 if (!isset($this->isAnnotation[$name])) { |
|
418 if (is_subclass_of($name, 'Doctrine\Common\Annotations\Annotation')) { |
|
419 $this->isAnnotation[$name] = true; |
|
420 } else { |
|
421 $reflClass = new \ReflectionClass($name); |
|
422 $this->isAnnotation[$name] = strpos($reflClass->getDocComment(), "@Annotation") !== false; |
|
423 } |
|
424 } |
|
425 return $this->isAnnotation[$name]; |
|
426 } |
|
427 |
|
428 private function newAnnotation($name, $values) |
|
429 { |
|
430 if ($this->creationFn !== null) { |
|
431 $fn = $this->creationFn; |
|
432 return $fn($name, $values); |
|
433 } |
|
434 |
|
435 return new $name($values); |
|
436 } |
|
437 |
|
438 /** |
|
439 * Values ::= Array | Value {"," Value}* |
|
440 * |
|
441 * @return array |
|
442 */ |
|
443 private function Values() |
|
444 { |
|
445 $values = array(); |
|
446 |
|
447 // Handle the case of a single array as value, i.e. @Foo({....}) |
|
448 if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) { |
|
449 $values['value'] = $this->Value(); |
|
450 return $values; |
|
451 } |
|
452 |
|
453 $values[] = $this->Value(); |
|
454 |
|
455 while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { |
|
456 $this->match(DocLexer::T_COMMA); |
|
457 $token = $this->lexer->lookahead; |
|
458 $value = $this->Value(); |
|
459 |
|
460 if ( ! is_object($value) && ! is_array($value)) { |
|
461 $this->syntaxError('Value', $token); |
|
462 } |
|
463 |
|
464 $values[] = $value; |
|
465 } |
|
466 |
|
467 foreach ($values as $k => $value) { |
|
468 if (is_object($value) && $value instanceof \stdClass) { |
|
469 $values[$value->name] = $value->value; |
|
470 } else if ( ! isset($values['value'])){ |
|
471 $values['value'] = $value; |
|
472 } else { |
|
473 if ( ! is_array($values['value'])) { |
|
474 $values['value'] = array($values['value']); |
|
475 } |
|
476 |
|
477 $values['value'][] = $value; |
|
478 } |
|
479 |
|
480 unset($values[$k]); |
|
481 } |
|
482 |
|
483 return $values; |
|
484 } |
|
485 |
|
486 /** |
|
487 * Value ::= PlainValue | FieldAssignment |
|
488 * |
|
489 * @return mixed |
|
490 */ |
|
491 private function Value() |
|
492 { |
|
493 $peek = $this->lexer->glimpse(); |
|
494 |
|
495 if (DocLexer::T_EQUALS === $peek['type']) { |
|
496 return $this->FieldAssignment(); |
|
497 } |
|
498 |
|
499 return $this->PlainValue(); |
|
500 } |
|
501 |
|
502 /** |
|
503 * PlainValue ::= integer | string | float | boolean | Array | Annotation |
|
504 * |
|
505 * @return mixed |
|
506 */ |
|
507 private function PlainValue() |
|
508 { |
|
509 if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) { |
|
510 return $this->Arrayx(); |
|
511 } |
|
512 |
|
513 if ($this->lexer->isNextToken(DocLexer::T_AT)) { |
|
514 return $this->Annotation(); |
|
515 } |
|
516 |
|
517 switch ($this->lexer->lookahead['type']) { |
|
518 case DocLexer::T_STRING: |
|
519 $this->match(DocLexer::T_STRING); |
|
520 return $this->lexer->token['value']; |
|
521 |
|
522 case DocLexer::T_INTEGER: |
|
523 $this->match(DocLexer::T_INTEGER); |
|
524 return (int)$this->lexer->token['value']; |
|
525 |
|
526 case DocLexer::T_FLOAT: |
|
527 $this->match(DocLexer::T_FLOAT); |
|
528 return (float)$this->lexer->token['value']; |
|
529 |
|
530 case DocLexer::T_TRUE: |
|
531 $this->match(DocLexer::T_TRUE); |
|
532 return true; |
|
533 |
|
534 case DocLexer::T_FALSE: |
|
535 $this->match(DocLexer::T_FALSE); |
|
536 return false; |
|
537 |
|
538 case DocLexer::T_NULL: |
|
539 $this->match(DocLexer::T_NULL); |
|
540 return null; |
|
541 |
|
542 default: |
|
543 $this->syntaxError('PlainValue'); |
|
544 } |
|
545 } |
|
546 |
|
547 /** |
|
548 * FieldAssignment ::= FieldName "=" PlainValue |
|
549 * FieldName ::= identifier |
|
550 * |
|
551 * @return array |
|
552 */ |
|
553 private function FieldAssignment() |
|
554 { |
|
555 $this->match(DocLexer::T_IDENTIFIER); |
|
556 $fieldName = $this->lexer->token['value']; |
|
557 |
|
558 $this->match(DocLexer::T_EQUALS); |
|
559 |
|
560 $item = new \stdClass(); |
|
561 $item->name = $fieldName; |
|
562 $item->value = $this->PlainValue(); |
|
563 |
|
564 return $item; |
|
565 } |
|
566 |
|
567 /** |
|
568 * Array ::= "{" ArrayEntry {"," ArrayEntry}* "}" |
|
569 * |
|
570 * @return array |
|
571 */ |
|
572 private function Arrayx() |
|
573 { |
|
574 $array = $values = array(); |
|
575 |
|
576 $this->match(DocLexer::T_OPEN_CURLY_BRACES); |
|
577 $values[] = $this->ArrayEntry(); |
|
578 |
|
579 while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { |
|
580 $this->match(DocLexer::T_COMMA); |
|
581 $values[] = $this->ArrayEntry(); |
|
582 } |
|
583 |
|
584 $this->match(DocLexer::T_CLOSE_CURLY_BRACES); |
|
585 |
|
586 foreach ($values as $value) { |
|
587 list ($key, $val) = $value; |
|
588 |
|
589 if ($key !== null) { |
|
590 $array[$key] = $val; |
|
591 } else { |
|
592 $array[] = $val; |
|
593 } |
|
594 } |
|
595 |
|
596 return $array; |
|
597 } |
|
598 |
|
599 /** |
|
600 * ArrayEntry ::= Value | KeyValuePair |
|
601 * KeyValuePair ::= Key "=" PlainValue |
|
602 * Key ::= string | integer |
|
603 * |
|
604 * @return array |
|
605 */ |
|
606 private function ArrayEntry() |
|
607 { |
|
608 $peek = $this->lexer->glimpse(); |
|
609 |
|
610 if (DocLexer::T_EQUALS === $peek['type']) { |
|
611 $this->match( |
|
612 $this->lexer->isNextToken(DocLexer::T_INTEGER) ? DocLexer::T_INTEGER : DocLexer::T_STRING |
|
613 ); |
|
614 |
|
615 $key = $this->lexer->token['value']; |
|
616 $this->match(DocLexer::T_EQUALS); |
|
617 |
|
618 return array($key, $this->PlainValue()); |
|
619 } |
|
620 |
|
621 return array(null, $this->Value()); |
|
622 } |
|
623 } |