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 | use Doctrine\Common\Annotations\Annotation\Target; |
---|
25 | use Doctrine\Common\Annotations\Annotation\Attribute; |
---|
26 | use Doctrine\Common\Annotations\Annotation\Attributes; |
---|
27 | |
---|
28 | /** |
---|
29 | * A parser for docblock annotations. |
---|
30 | * |
---|
31 | * It is strongly discouraged to change the default annotation parsing process. |
---|
32 | * |
---|
33 | * @author Benjamin Eberlei <kontakt@beberlei.de> |
---|
34 | * @author Guilherme Blanco <guilhermeblanco@hotmail.com> |
---|
35 | * @author Jonathan Wage <jonwage@gmail.com> |
---|
36 | * @author Roman Borschel <roman@code-factory.org> |
---|
37 | * @author Johannes M. Schmitt <schmittjoh@gmail.com> |
---|
38 | * @author Fabio B. Silva <fabio.bat.silva@gmail.com> |
---|
39 | */ |
---|
40 | final class DocParser |
---|
41 | { |
---|
42 | /** |
---|
43 | * An array of all valid tokens for a class name. |
---|
44 | * |
---|
45 | * @var array |
---|
46 | */ |
---|
47 | private static $classIdentifiers = array(DocLexer::T_IDENTIFIER, DocLexer::T_TRUE, DocLexer::T_FALSE, DocLexer::T_NULL); |
---|
48 | |
---|
49 | /** |
---|
50 | * The lexer. |
---|
51 | * |
---|
52 | * @var Doctrine\Common\Annotations\DocLexer |
---|
53 | */ |
---|
54 | private $lexer; |
---|
55 | |
---|
56 | /** |
---|
57 | * Current target context |
---|
58 | * |
---|
59 | * @var string |
---|
60 | */ |
---|
61 | private $target; |
---|
62 | |
---|
63 | /** |
---|
64 | * Doc Parser used to collect annotation target |
---|
65 | * |
---|
66 | * @var Doctrine\Common\Annotations\DocParser |
---|
67 | */ |
---|
68 | private static $metadataParser; |
---|
69 | |
---|
70 | /** |
---|
71 | * Flag to control if the current annotation is nested or not. |
---|
72 | * |
---|
73 | * @var boolean |
---|
74 | */ |
---|
75 | private $isNestedAnnotation = false; |
---|
76 | |
---|
77 | /** |
---|
78 | * Hashmap containing all use-statements that are to be used when parsing |
---|
79 | * the given doc block. |
---|
80 | * |
---|
81 | * @var array |
---|
82 | */ |
---|
83 | private $imports = array(); |
---|
84 | |
---|
85 | /** |
---|
86 | * This hashmap is used internally to cache results of class_exists() |
---|
87 | * look-ups. |
---|
88 | * |
---|
89 | * @var array |
---|
90 | */ |
---|
91 | private $classExists = array(); |
---|
92 | |
---|
93 | /** |
---|
94 | * Whether annotations that have not been imported should be ignored. |
---|
95 | * |
---|
96 | * @var boolean |
---|
97 | */ |
---|
98 | private $ignoreNotImportedAnnotations = false; |
---|
99 | |
---|
100 | /** |
---|
101 | * An array of default namespaces if operating in simple mode. |
---|
102 | * |
---|
103 | * @var array |
---|
104 | */ |
---|
105 | private $namespaces = array(); |
---|
106 | |
---|
107 | /** |
---|
108 | * A list with annotations that are not causing exceptions when not resolved to an annotation class. |
---|
109 | * |
---|
110 | * The names must be the raw names as used in the class, not the fully qualified |
---|
111 | * class names. |
---|
112 | * |
---|
113 | * @var array |
---|
114 | */ |
---|
115 | private $ignoredAnnotationNames = array(); |
---|
116 | |
---|
117 | /** |
---|
118 | * @var string |
---|
119 | */ |
---|
120 | private $context = ''; |
---|
121 | |
---|
122 | /** |
---|
123 | * Hash-map for caching annotation metadata |
---|
124 | * @var array |
---|
125 | */ |
---|
126 | private static $annotationMetadata = array( |
---|
127 | 'Doctrine\Common\Annotations\Annotation\Target' => array( |
---|
128 | 'is_annotation' => true, |
---|
129 | 'has_constructor' => true, |
---|
130 | 'properties' => array(), |
---|
131 | 'targets_literal' => 'ANNOTATION_CLASS', |
---|
132 | 'targets' => Target::TARGET_CLASS, |
---|
133 | 'default_property' => 'value', |
---|
134 | 'attribute_types' => array( |
---|
135 | 'value' => array( |
---|
136 | 'required' => false, |
---|
137 | 'type' =>'array', |
---|
138 | 'array_type'=>'string', |
---|
139 | 'value' =>'array<string>' |
---|
140 | ) |
---|
141 | ), |
---|
142 | ), |
---|
143 | 'Doctrine\Common\Annotations\Annotation\Attribute' => array( |
---|
144 | 'is_annotation' => true, |
---|
145 | 'has_constructor' => false, |
---|
146 | 'targets_literal' => 'ANNOTATION_ANNOTATION', |
---|
147 | 'targets' => Target::TARGET_ANNOTATION, |
---|
148 | 'default_property' => 'name', |
---|
149 | 'properties' => array( |
---|
150 | 'name' => 'name', |
---|
151 | 'type' => 'type', |
---|
152 | 'required' => 'required' |
---|
153 | ), |
---|
154 | 'attribute_types' => array( |
---|
155 | 'value' => array( |
---|
156 | 'required' => true, |
---|
157 | 'type' =>'string', |
---|
158 | 'value' =>'string' |
---|
159 | ), |
---|
160 | 'type' => array( |
---|
161 | 'required' =>true, |
---|
162 | 'type' =>'string', |
---|
163 | 'value' =>'string' |
---|
164 | ), |
---|
165 | 'required' => array( |
---|
166 | 'required' =>false, |
---|
167 | 'type' =>'boolean', |
---|
168 | 'value' =>'boolean' |
---|
169 | ) |
---|
170 | ), |
---|
171 | ), |
---|
172 | 'Doctrine\Common\Annotations\Annotation\Attributes' => array( |
---|
173 | 'is_annotation' => true, |
---|
174 | 'has_constructor' => false, |
---|
175 | 'targets_literal' => 'ANNOTATION_CLASS', |
---|
176 | 'targets' => Target::TARGET_CLASS, |
---|
177 | 'default_property' => 'value', |
---|
178 | 'properties' => array( |
---|
179 | 'value' => 'value' |
---|
180 | ), |
---|
181 | 'attribute_types' => array( |
---|
182 | 'value' => array( |
---|
183 | 'type' =>'array', |
---|
184 | 'required' =>true, |
---|
185 | 'array_type'=>'Doctrine\Common\Annotations\Annotation\Attribute', |
---|
186 | 'value' =>'array<Doctrine\Common\Annotations\Annotation\Attribute>' |
---|
187 | ) |
---|
188 | ), |
---|
189 | ), |
---|
190 | ); |
---|
191 | |
---|
192 | /** |
---|
193 | * Hash-map for handle types declaration |
---|
194 | * |
---|
195 | * @var array |
---|
196 | */ |
---|
197 | private static $typeMap = array( |
---|
198 | 'float' => 'double', |
---|
199 | 'bool' => 'boolean', |
---|
200 | // allow uppercase Boolean in honor of George Boole |
---|
201 | 'Boolean' => 'boolean', |
---|
202 | 'int' => 'integer', |
---|
203 | ); |
---|
204 | |
---|
205 | /** |
---|
206 | * Constructs a new DocParser. |
---|
207 | */ |
---|
208 | public function __construct() |
---|
209 | { |
---|
210 | $this->lexer = new DocLexer; |
---|
211 | } |
---|
212 | |
---|
213 | /** |
---|
214 | * Sets the annotation names that are ignored during the parsing process. |
---|
215 | * |
---|
216 | * The names are supposed to be the raw names as used in the class, not the |
---|
217 | * fully qualified class names. |
---|
218 | * |
---|
219 | * @param array $names |
---|
220 | */ |
---|
221 | public function setIgnoredAnnotationNames(array $names) |
---|
222 | { |
---|
223 | $this->ignoredAnnotationNames = $names; |
---|
224 | } |
---|
225 | |
---|
226 | public function setIgnoreNotImportedAnnotations($bool) |
---|
227 | { |
---|
228 | $this->ignoreNotImportedAnnotations = (Boolean) $bool; |
---|
229 | } |
---|
230 | |
---|
231 | /** |
---|
232 | * Sets the default namespaces. |
---|
233 | * @param array $namespaces |
---|
234 | */ |
---|
235 | public function addNamespace($namespace) |
---|
236 | { |
---|
237 | if ($this->imports) { |
---|
238 | throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); |
---|
239 | } |
---|
240 | $this->namespaces[] = $namespace; |
---|
241 | } |
---|
242 | |
---|
243 | public function setImports(array $imports) |
---|
244 | { |
---|
245 | if ($this->namespaces) { |
---|
246 | throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); |
---|
247 | } |
---|
248 | $this->imports = $imports; |
---|
249 | } |
---|
250 | |
---|
251 | /** |
---|
252 | * Sets current target context as bitmask. |
---|
253 | * |
---|
254 | * @param integer $target |
---|
255 | */ |
---|
256 | public function setTarget($target) |
---|
257 | { |
---|
258 | $this->target = $target; |
---|
259 | } |
---|
260 | |
---|
261 | /** |
---|
262 | * Parses the given docblock string for annotations. |
---|
263 | * |
---|
264 | * @param string $input The docblock string to parse. |
---|
265 | * @param string $context The parsing context. |
---|
266 | * @return array Array of annotations. If no annotations are found, an empty array is returned. |
---|
267 | */ |
---|
268 | public function parse($input, $context = '') |
---|
269 | { |
---|
270 | if (false === $pos = strpos($input, '@')) { |
---|
271 | return array(); |
---|
272 | } |
---|
273 | |
---|
274 | // also parse whatever character is before the @ |
---|
275 | if ($pos > 0) { |
---|
276 | $pos -= 1; |
---|
277 | } |
---|
278 | |
---|
279 | $this->context = $context; |
---|
280 | $this->lexer->setInput(trim(substr($input, $pos), '* /')); |
---|
281 | $this->lexer->moveNext(); |
---|
282 | |
---|
283 | return $this->Annotations(); |
---|
284 | } |
---|
285 | |
---|
286 | /** |
---|
287 | * Attempts to match the given token with the current lookahead token. |
---|
288 | * If they match, updates the lookahead token; otherwise raises a syntax error. |
---|
289 | * |
---|
290 | * @param int Token type. |
---|
291 | * @return bool True if tokens match; false otherwise. |
---|
292 | */ |
---|
293 | private function match($token) |
---|
294 | { |
---|
295 | if ( ! $this->lexer->isNextToken($token) ) { |
---|
296 | $this->syntaxError($this->lexer->getLiteral($token)); |
---|
297 | } |
---|
298 | |
---|
299 | return $this->lexer->moveNext(); |
---|
300 | } |
---|
301 | |
---|
302 | /** |
---|
303 | * Attempts to match the current lookahead token with any of the given tokens. |
---|
304 | * |
---|
305 | * If any of them matches, this method updates the lookahead token; otherwise |
---|
306 | * a syntax error is raised. |
---|
307 | * |
---|
308 | * @param array $tokens |
---|
309 | * @return bool |
---|
310 | */ |
---|
311 | private function matchAny(array $tokens) |
---|
312 | { |
---|
313 | if ( ! $this->lexer->isNextTokenAny($tokens)) { |
---|
314 | $this->syntaxError(implode(' or ', array_map(array($this->lexer, 'getLiteral'), $tokens))); |
---|
315 | } |
---|
316 | |
---|
317 | return $this->lexer->moveNext(); |
---|
318 | } |
---|
319 | |
---|
320 | /** |
---|
321 | * Generates a new syntax error. |
---|
322 | * |
---|
323 | * @param string $expected Expected string. |
---|
324 | * @param array $token Optional token. |
---|
325 | * @throws SyntaxException |
---|
326 | */ |
---|
327 | private function syntaxError($expected, $token = null) |
---|
328 | { |
---|
329 | if ($token === null) { |
---|
330 | $token = $this->lexer->lookahead; |
---|
331 | } |
---|
332 | |
---|
333 | $message = "Expected {$expected}, got "; |
---|
334 | |
---|
335 | if ($this->lexer->lookahead === null) { |
---|
336 | $message .= 'end of string'; |
---|
337 | } else { |
---|
338 | $message .= "'{$token['value']}' at position {$token['position']}"; |
---|
339 | } |
---|
340 | |
---|
341 | if (strlen($this->context)) { |
---|
342 | $message .= ' in ' . $this->context; |
---|
343 | } |
---|
344 | |
---|
345 | $message .= '.'; |
---|
346 | |
---|
347 | throw AnnotationException::syntaxError($message); |
---|
348 | } |
---|
349 | |
---|
350 | /** |
---|
351 | * Attempt to check if a class exists or not. This never goes through the PHP autoloading mechanism |
---|
352 | * but uses the {@link AnnotationRegistry} to load classes. |
---|
353 | * |
---|
354 | * @param string $fqcn |
---|
355 | * @return boolean |
---|
356 | */ |
---|
357 | private function classExists($fqcn) |
---|
358 | { |
---|
359 | if (isset($this->classExists[$fqcn])) { |
---|
360 | return $this->classExists[$fqcn]; |
---|
361 | } |
---|
362 | |
---|
363 | // first check if the class already exists, maybe loaded through another AnnotationReader |
---|
364 | if (class_exists($fqcn, false)) { |
---|
365 | return $this->classExists[$fqcn] = true; |
---|
366 | } |
---|
367 | |
---|
368 | // final check, does this class exist? |
---|
369 | return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn); |
---|
370 | } |
---|
371 | |
---|
372 | /** |
---|
373 | * Collects parsing metadata for a given annotation class |
---|
374 | * |
---|
375 | * @param string $name The annotation name |
---|
376 | */ |
---|
377 | private function collectAnnotationMetadata($name) |
---|
378 | { |
---|
379 | if (self::$metadataParser == null){ |
---|
380 | self::$metadataParser = new self(); |
---|
381 | self::$metadataParser->setTarget(Target::TARGET_CLASS); |
---|
382 | self::$metadataParser->setIgnoreNotImportedAnnotations(true); |
---|
383 | self::$metadataParser->setImports(array( |
---|
384 | 'target' => 'Doctrine\Common\Annotations\Annotation\Target', |
---|
385 | 'attribute' => 'Doctrine\Common\Annotations\Annotation\Attribute', |
---|
386 | 'attributes' => 'Doctrine\Common\Annotations\Annotation\Attributes' |
---|
387 | )); |
---|
388 | AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Target.php'); |
---|
389 | AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attribute.php'); |
---|
390 | AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attributes.php'); |
---|
391 | } |
---|
392 | |
---|
393 | $class = new \ReflectionClass($name); |
---|
394 | $docComment = $class->getDocComment(); |
---|
395 | |
---|
396 | // Sets default values for annotation metadata |
---|
397 | $metadata = array( |
---|
398 | 'default_property' => null, |
---|
399 | 'has_constructor' => (null !== $constructor = $class->getConstructor()) && $constructor->getNumberOfParameters() > 0, |
---|
400 | 'properties' => array(), |
---|
401 | 'property_types' => array(), |
---|
402 | 'attribute_types' => array(), |
---|
403 | 'targets_literal' => null, |
---|
404 | 'targets' => Target::TARGET_ALL, |
---|
405 | 'is_annotation' => false !== strpos($docComment, '@Annotation'), |
---|
406 | ); |
---|
407 | |
---|
408 | // verify that the class is really meant to be an annotation |
---|
409 | if ($metadata['is_annotation']) { |
---|
410 | foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) { |
---|
411 | if ($annotation instanceof Target) { |
---|
412 | $metadata['targets'] = $annotation->targets; |
---|
413 | $metadata['targets_literal'] = $annotation->literal; |
---|
414 | |
---|
415 | } elseif ($annotation instanceof Attributes) { |
---|
416 | foreach ($annotation->value as $attrib) { |
---|
417 | // handle internal type declaration |
---|
418 | $type = isset(self::$typeMap[$attrib->type]) ? self::$typeMap[$attrib->type] : $attrib->type; |
---|
419 | |
---|
420 | // handle the case if the property type is mixed |
---|
421 | if ('mixed' !== $type) { |
---|
422 | // Checks if the property has array<type> |
---|
423 | if (false !== $pos = strpos($type, '<')) { |
---|
424 | $arrayType = substr($type, $pos+1, -1); |
---|
425 | $type = 'array'; |
---|
426 | |
---|
427 | if (isset(self::$typeMap[$arrayType])) { |
---|
428 | $arrayType = self::$typeMap[$arrayType]; |
---|
429 | } |
---|
430 | |
---|
431 | $metadata['attribute_types'][$attrib->name]['array_type'] = $arrayType; |
---|
432 | } |
---|
433 | |
---|
434 | $metadata['attribute_types'][$attrib->name]['type'] = $type; |
---|
435 | $metadata['attribute_types'][$attrib->name]['value'] = $attrib->type; |
---|
436 | $metadata['attribute_types'][$attrib->name]['required'] = $attrib->required; |
---|
437 | } |
---|
438 | } |
---|
439 | } |
---|
440 | } |
---|
441 | |
---|
442 | // if not has a constructor will inject values into public properties |
---|
443 | if (false === $metadata['has_constructor']) { |
---|
444 | // collect all public properties |
---|
445 | foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { |
---|
446 | $metadata['properties'][$property->name] = $property->name; |
---|
447 | |
---|
448 | // checks if the property has @var annotation |
---|
449 | if ((false !== $propertyComment = $property->getDocComment()) |
---|
450 | && false !== strpos($propertyComment, '@var') |
---|
451 | && preg_match('/@var\s+([^\s]+)/',$propertyComment, $matches)) { |
---|
452 | // literal type declaration |
---|
453 | $value = $matches[1]; |
---|
454 | |
---|
455 | // handle internal type declaration |
---|
456 | $type = isset(self::$typeMap[$value]) ? self::$typeMap[$value] : $value; |
---|
457 | |
---|
458 | // handle the case if the property type is mixed |
---|
459 | if ('mixed' !== $type) { |
---|
460 | // Checks if the property has @var array<type> annotation |
---|
461 | if (false !== $pos = strpos($type, '<')) { |
---|
462 | $arrayType = substr($type, $pos+1, -1); |
---|
463 | $type = 'array'; |
---|
464 | |
---|
465 | if (isset(self::$typeMap[$arrayType])) { |
---|
466 | $arrayType = self::$typeMap[$arrayType]; |
---|
467 | } |
---|
468 | |
---|
469 | $metadata['attribute_types'][$property->name]['array_type'] = $arrayType; |
---|
470 | } |
---|
471 | |
---|
472 | $metadata['attribute_types'][$property->name]['type'] = $type; |
---|
473 | $metadata['attribute_types'][$property->name]['value'] = $value; |
---|
474 | $metadata['attribute_types'][$property->name]['required'] = false !== strpos($propertyComment, '@Required'); |
---|
475 | } |
---|
476 | } |
---|
477 | } |
---|
478 | |
---|
479 | // choose the first property as default property |
---|
480 | $metadata['default_property'] = reset($metadata['properties']); |
---|
481 | } |
---|
482 | } |
---|
483 | |
---|
484 | self::$annotationMetadata[$name] = $metadata; |
---|
485 | } |
---|
486 | |
---|
487 | /** |
---|
488 | * Annotations ::= Annotation {[ "*" ]* [Annotation]}* |
---|
489 | * |
---|
490 | * @return array |
---|
491 | */ |
---|
492 | private function Annotations() |
---|
493 | { |
---|
494 | $annotations = array(); |
---|
495 | |
---|
496 | while (null !== $this->lexer->lookahead) { |
---|
497 | if (DocLexer::T_AT !== $this->lexer->lookahead['type']) { |
---|
498 | $this->lexer->moveNext(); |
---|
499 | continue; |
---|
500 | } |
---|
501 | |
---|
502 | // make sure the @ is preceded by non-catchable pattern |
---|
503 | if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) { |
---|
504 | $this->lexer->moveNext(); |
---|
505 | continue; |
---|
506 | } |
---|
507 | |
---|
508 | // make sure the @ is followed by either a namespace separator, or |
---|
509 | // an identifier token |
---|
510 | if ((null === $peek = $this->lexer->glimpse()) |
---|
511 | || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true)) |
---|
512 | || $peek['position'] !== $this->lexer->lookahead['position'] + 1) { |
---|
513 | $this->lexer->moveNext(); |
---|
514 | continue; |
---|
515 | } |
---|
516 | |
---|
517 | $this->isNestedAnnotation = false; |
---|
518 | if (false !== $annot = $this->Annotation()) { |
---|
519 | $annotations[] = $annot; |
---|
520 | } |
---|
521 | } |
---|
522 | |
---|
523 | return $annotations; |
---|
524 | } |
---|
525 | |
---|
526 | /** |
---|
527 | * Annotation ::= "@" AnnotationName ["(" [Values] ")"] |
---|
528 | * AnnotationName ::= QualifiedName | SimpleName |
---|
529 | * QualifiedName ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName |
---|
530 | * NameSpacePart ::= identifier | null | false | true |
---|
531 | * SimpleName ::= identifier | null | false | true |
---|
532 | * |
---|
533 | * @return mixed False if it is not a valid annotation. |
---|
534 | */ |
---|
535 | private function Annotation() |
---|
536 | { |
---|
537 | $this->match(DocLexer::T_AT); |
---|
538 | |
---|
539 | // check if we have an annotation |
---|
540 | if ($this->lexer->isNextTokenAny(self::$classIdentifiers)) { |
---|
541 | $this->lexer->moveNext(); |
---|
542 | $name = $this->lexer->token['value']; |
---|
543 | } else if ($this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) { |
---|
544 | $name = ''; |
---|
545 | } else { |
---|
546 | $this->syntaxError('namespace separator or identifier'); |
---|
547 | } |
---|
548 | |
---|
549 | while ($this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value']) && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) { |
---|
550 | $this->match(DocLexer::T_NAMESPACE_SEPARATOR); |
---|
551 | $this->matchAny(self::$classIdentifiers); |
---|
552 | $name .= '\\'.$this->lexer->token['value']; |
---|
553 | } |
---|
554 | |
---|
555 | // only process names which are not fully qualified, yet |
---|
556 | // fully qualified names must start with a \ |
---|
557 | $originalName = $name; |
---|
558 | if ('\\' !== $name[0]) { |
---|
559 | $alias = (false === $pos = strpos($name, '\\'))? $name : substr($name, 0, $pos); |
---|
560 | |
---|
561 | $found = false; |
---|
562 | if ($this->namespaces) { |
---|
563 | foreach ($this->namespaces as $namespace) { |
---|
564 | if ($this->classExists($namespace.'\\'.$name)) { |
---|
565 | $name = $namespace.'\\'.$name; |
---|
566 | $found = true; |
---|
567 | break; |
---|
568 | } |
---|
569 | } |
---|
570 | } elseif (isset($this->imports[$loweredAlias = strtolower($alias)])) { |
---|
571 | if (false !== $pos) { |
---|
572 | $name = $this->imports[$loweredAlias].substr($name, $pos); |
---|
573 | } else { |
---|
574 | $name = $this->imports[$loweredAlias]; |
---|
575 | } |
---|
576 | $found = true; |
---|
577 | } elseif (isset($this->imports['__NAMESPACE__']) && $this->classExists($this->imports['__NAMESPACE__'].'\\'.$name)) { |
---|
578 | $name = $this->imports['__NAMESPACE__'].'\\'.$name; |
---|
579 | $found = true; |
---|
580 | } elseif ($this->classExists($name)) { |
---|
581 | $found = true; |
---|
582 | } |
---|
583 | |
---|
584 | if (!$found) { |
---|
585 | if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) { |
---|
586 | return false; |
---|
587 | } |
---|
588 | |
---|
589 | throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s was never imported. Did you maybe forget to add a "use" statement for this annotation?', $name, $this->context)); |
---|
590 | } |
---|
591 | } |
---|
592 | |
---|
593 | if (!$this->classExists($name)) { |
---|
594 | throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context)); |
---|
595 | } |
---|
596 | |
---|
597 | // at this point, $name contains the fully qualified class name of the |
---|
598 | // annotation, and it is also guaranteed that this class exists, and |
---|
599 | // that it is loaded |
---|
600 | |
---|
601 | |
---|
602 | // collects the metadata annotation only if there is not yet |
---|
603 | if (!isset(self::$annotationMetadata[$name])) { |
---|
604 | $this->collectAnnotationMetadata($name); |
---|
605 | } |
---|
606 | |
---|
607 | // verify that the class is really meant to be an annotation and not just any ordinary class |
---|
608 | if (self::$annotationMetadata[$name]['is_annotation'] === false) { |
---|
609 | if (isset($this->ignoredAnnotationNames[$originalName])) { |
---|
610 | return false; |
---|
611 | } |
---|
612 | |
---|
613 | throw AnnotationException::semanticalError(sprintf('The class "%s" is not annotated with @Annotation. Are you sure this class can be used as annotation? If so, then you need to add @Annotation to the _class_ doc comment of "%s". If it is indeed no annotation, then you need to add @IgnoreAnnotation("%s") to the _class_ doc comment of %s.', $name, $name, $originalName, $this->context)); |
---|
614 | } |
---|
615 | |
---|
616 | //if target is nested annotation |
---|
617 | $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target; |
---|
618 | |
---|
619 | // Next will be nested |
---|
620 | $this->isNestedAnnotation = true; |
---|
621 | |
---|
622 | //if annotation does not support current target |
---|
623 | if (0 === (self::$annotationMetadata[$name]['targets'] & $target) && $target) { |
---|
624 | throw AnnotationException::semanticalError( |
---|
625 | sprintf('Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.', |
---|
626 | $originalName, $this->context, self::$annotationMetadata[$name]['targets_literal']) |
---|
627 | ); |
---|
628 | } |
---|
629 | |
---|
630 | $values = array(); |
---|
631 | if ($this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) { |
---|
632 | $this->match(DocLexer::T_OPEN_PARENTHESIS); |
---|
633 | |
---|
634 | if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) { |
---|
635 | $values = $this->Values(); |
---|
636 | } |
---|
637 | |
---|
638 | $this->match(DocLexer::T_CLOSE_PARENTHESIS); |
---|
639 | } |
---|
640 | |
---|
641 | // checks all declared attributes |
---|
642 | foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) { |
---|
643 | if ($property === self::$annotationMetadata[$name]['default_property'] |
---|
644 | && !isset($values[$property]) && isset($values['value'])) { |
---|
645 | $property = 'value'; |
---|
646 | } |
---|
647 | |
---|
648 | // handle a not given attribute or null value |
---|
649 | if (!isset($values[$property])) { |
---|
650 | if ($type['required']) { |
---|
651 | throw AnnotationException::requiredError($property, $originalName, $this->context, 'a(n) '.$type['value']); |
---|
652 | } |
---|
653 | |
---|
654 | continue; |
---|
655 | } |
---|
656 | |
---|
657 | if ($type['type'] === 'array') { |
---|
658 | // handle the case of a single value |
---|
659 | if (!is_array($values[$property])) { |
---|
660 | $values[$property] = array($values[$property]); |
---|
661 | } |
---|
662 | |
---|
663 | // checks if the attribute has array type declaration, such as "array<string>" |
---|
664 | if (isset($type['array_type'])) { |
---|
665 | foreach ($values[$property] as $item) { |
---|
666 | if (gettype($item) !== $type['array_type'] && !$item instanceof $type['array_type']) { |
---|
667 | throw AnnotationException::typeError($property, $originalName, $this->context, 'either a(n) '.$type['array_type'].', or an array of '.$type['array_type'].'s', $item); |
---|
668 | } |
---|
669 | } |
---|
670 | } |
---|
671 | } elseif (gettype($values[$property]) !== $type['type'] && !$values[$property] instanceof $type['type']) { |
---|
672 | throw AnnotationException::typeError($property, $originalName, $this->context, 'a(n) '.$type['value'], $values[$property]); |
---|
673 | } |
---|
674 | } |
---|
675 | |
---|
676 | // check if the annotation expects values via the constructor, |
---|
677 | // or directly injected into public properties |
---|
678 | if (self::$annotationMetadata[$name]['has_constructor'] === true) { |
---|
679 | return new $name($values); |
---|
680 | } |
---|
681 | |
---|
682 | $instance = new $name(); |
---|
683 | foreach ($values as $property => $value) { |
---|
684 | if (!isset(self::$annotationMetadata[$name]['properties'][$property])) { |
---|
685 | if ('value' !== $property) { |
---|
686 | throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not have a property named "%s". Available properties: %s', $originalName, $this->context, $property, implode(', ', self::$annotationMetadata[$name]['properties']))); |
---|
687 | } |
---|
688 | |
---|
689 | // handle the case if the property has no annotations |
---|
690 | if (!$property = self::$annotationMetadata[$name]['default_property']) { |
---|
691 | throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not accept any values, but got %s.', $originalName, $this->context, json_encode($values))); |
---|
692 | } |
---|
693 | } |
---|
694 | |
---|
695 | $instance->{$property} = $value; |
---|
696 | } |
---|
697 | |
---|
698 | return $instance; |
---|
699 | } |
---|
700 | |
---|
701 | /** |
---|
702 | * Values ::= Array | Value {"," Value}* |
---|
703 | * |
---|
704 | * @return array |
---|
705 | */ |
---|
706 | private function Values() |
---|
707 | { |
---|
708 | $values = array(); |
---|
709 | |
---|
710 | // Handle the case of a single array as value, i.e. @Foo({....}) |
---|
711 | if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) { |
---|
712 | $values['value'] = $this->Value(); |
---|
713 | return $values; |
---|
714 | } |
---|
715 | |
---|
716 | $values[] = $this->Value(); |
---|
717 | |
---|
718 | while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { |
---|
719 | $this->match(DocLexer::T_COMMA); |
---|
720 | $token = $this->lexer->lookahead; |
---|
721 | $value = $this->Value(); |
---|
722 | |
---|
723 | if ( ! is_object($value) && ! is_array($value)) { |
---|
724 | $this->syntaxError('Value', $token); |
---|
725 | } |
---|
726 | |
---|
727 | $values[] = $value; |
---|
728 | } |
---|
729 | |
---|
730 | foreach ($values as $k => $value) { |
---|
731 | if (is_object($value) && $value instanceof \stdClass) { |
---|
732 | $values[$value->name] = $value->value; |
---|
733 | } else if ( ! isset($values['value'])){ |
---|
734 | $values['value'] = $value; |
---|
735 | } else { |
---|
736 | if ( ! is_array($values['value'])) { |
---|
737 | $values['value'] = array($values['value']); |
---|
738 | } |
---|
739 | |
---|
740 | $values['value'][] = $value; |
---|
741 | } |
---|
742 | |
---|
743 | unset($values[$k]); |
---|
744 | } |
---|
745 | |
---|
746 | return $values; |
---|
747 | } |
---|
748 | |
---|
749 | /** |
---|
750 | * Value ::= PlainValue | FieldAssignment |
---|
751 | * |
---|
752 | * @return mixed |
---|
753 | */ |
---|
754 | private function Value() |
---|
755 | { |
---|
756 | $peek = $this->lexer->glimpse(); |
---|
757 | |
---|
758 | if (DocLexer::T_EQUALS === $peek['type']) { |
---|
759 | return $this->FieldAssignment(); |
---|
760 | } |
---|
761 | |
---|
762 | return $this->PlainValue(); |
---|
763 | } |
---|
764 | |
---|
765 | /** |
---|
766 | * PlainValue ::= integer | string | float | boolean | Array | Annotation |
---|
767 | * |
---|
768 | * @return mixed |
---|
769 | */ |
---|
770 | private function PlainValue() |
---|
771 | { |
---|
772 | if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) { |
---|
773 | return $this->Arrayx(); |
---|
774 | } |
---|
775 | |
---|
776 | if ($this->lexer->isNextToken(DocLexer::T_AT)) { |
---|
777 | return $this->Annotation(); |
---|
778 | } |
---|
779 | |
---|
780 | switch ($this->lexer->lookahead['type']) { |
---|
781 | case DocLexer::T_STRING: |
---|
782 | $this->match(DocLexer::T_STRING); |
---|
783 | return $this->lexer->token['value']; |
---|
784 | |
---|
785 | case DocLexer::T_INTEGER: |
---|
786 | $this->match(DocLexer::T_INTEGER); |
---|
787 | return (int)$this->lexer->token['value']; |
---|
788 | |
---|
789 | case DocLexer::T_FLOAT: |
---|
790 | $this->match(DocLexer::T_FLOAT); |
---|
791 | return (float)$this->lexer->token['value']; |
---|
792 | |
---|
793 | case DocLexer::T_TRUE: |
---|
794 | $this->match(DocLexer::T_TRUE); |
---|
795 | return true; |
---|
796 | |
---|
797 | case DocLexer::T_FALSE: |
---|
798 | $this->match(DocLexer::T_FALSE); |
---|
799 | return false; |
---|
800 | |
---|
801 | case DocLexer::T_NULL: |
---|
802 | $this->match(DocLexer::T_NULL); |
---|
803 | return null; |
---|
804 | |
---|
805 | default: |
---|
806 | $this->syntaxError('PlainValue'); |
---|
807 | } |
---|
808 | } |
---|
809 | |
---|
810 | /** |
---|
811 | * FieldAssignment ::= FieldName "=" PlainValue |
---|
812 | * FieldName ::= identifier |
---|
813 | * |
---|
814 | * @return array |
---|
815 | */ |
---|
816 | private function FieldAssignment() |
---|
817 | { |
---|
818 | $this->match(DocLexer::T_IDENTIFIER); |
---|
819 | $fieldName = $this->lexer->token['value']; |
---|
820 | |
---|
821 | $this->match(DocLexer::T_EQUALS); |
---|
822 | |
---|
823 | $item = new \stdClass(); |
---|
824 | $item->name = $fieldName; |
---|
825 | $item->value = $this->PlainValue(); |
---|
826 | |
---|
827 | return $item; |
---|
828 | } |
---|
829 | |
---|
830 | /** |
---|
831 | * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}" |
---|
832 | * |
---|
833 | * @return array |
---|
834 | */ |
---|
835 | private function Arrayx() |
---|
836 | { |
---|
837 | $array = $values = array(); |
---|
838 | |
---|
839 | $this->match(DocLexer::T_OPEN_CURLY_BRACES); |
---|
840 | $values[] = $this->ArrayEntry(); |
---|
841 | |
---|
842 | while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { |
---|
843 | $this->match(DocLexer::T_COMMA); |
---|
844 | |
---|
845 | // optional trailing comma |
---|
846 | if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) { |
---|
847 | break; |
---|
848 | } |
---|
849 | |
---|
850 | $values[] = $this->ArrayEntry(); |
---|
851 | } |
---|
852 | |
---|
853 | $this->match(DocLexer::T_CLOSE_CURLY_BRACES); |
---|
854 | |
---|
855 | foreach ($values as $value) { |
---|
856 | list ($key, $val) = $value; |
---|
857 | |
---|
858 | if ($key !== null) { |
---|
859 | $array[$key] = $val; |
---|
860 | } else { |
---|
861 | $array[] = $val; |
---|
862 | } |
---|
863 | } |
---|
864 | |
---|
865 | return $array; |
---|
866 | } |
---|
867 | |
---|
868 | /** |
---|
869 | * ArrayEntry ::= Value | KeyValuePair |
---|
870 | * KeyValuePair ::= Key ("=" | ":") PlainValue |
---|
871 | * Key ::= string | integer |
---|
872 | * |
---|
873 | * @return array |
---|
874 | */ |
---|
875 | private function ArrayEntry() |
---|
876 | { |
---|
877 | $peek = $this->lexer->glimpse(); |
---|
878 | |
---|
879 | if (DocLexer::T_EQUALS === $peek['type'] |
---|
880 | || DocLexer::T_COLON === $peek['type']) { |
---|
881 | $this->matchAny(array(DocLexer::T_INTEGER, DocLexer::T_STRING)); |
---|
882 | |
---|
883 | $key = $this->lexer->token['value']; |
---|
884 | $this->matchAny(array(DocLexer::T_EQUALS, DocLexer::T_COLON)); |
---|
885 | |
---|
886 | return array($key, $this->PlainValue()); |
---|
887 | } |
---|
888 | |
---|
889 | return array(null, $this->Value()); |
---|
890 | } |
---|
891 | } |
---|