Mercurial > hg > isophonics-drupal-site
comparison vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocParser.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 7a779792577d |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
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 MIT license. For more information, see | |
17 * <http://www.doctrine-project.org>. | |
18 */ | |
19 | |
20 namespace Doctrine\Common\Annotations; | |
21 | |
22 use Doctrine\Common\Annotations\Annotation\Attribute; | |
23 use ReflectionClass; | |
24 use Doctrine\Common\Annotations\Annotation\Enum; | |
25 use Doctrine\Common\Annotations\Annotation\Target; | |
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( | |
48 DocLexer::T_IDENTIFIER, | |
49 DocLexer::T_TRUE, | |
50 DocLexer::T_FALSE, | |
51 DocLexer::T_NULL | |
52 ); | |
53 | |
54 /** | |
55 * The lexer. | |
56 * | |
57 * @var \Doctrine\Common\Annotations\DocLexer | |
58 */ | |
59 private $lexer; | |
60 | |
61 /** | |
62 * Current target context. | |
63 * | |
64 * @var string | |
65 */ | |
66 private $target; | |
67 | |
68 /** | |
69 * Doc parser used to collect annotation target. | |
70 * | |
71 * @var \Doctrine\Common\Annotations\DocParser | |
72 */ | |
73 private static $metadataParser; | |
74 | |
75 /** | |
76 * Flag to control if the current annotation is nested or not. | |
77 * | |
78 * @var boolean | |
79 */ | |
80 private $isNestedAnnotation = false; | |
81 | |
82 /** | |
83 * Hashmap containing all use-statements that are to be used when parsing | |
84 * the given doc block. | |
85 * | |
86 * @var array | |
87 */ | |
88 private $imports = array(); | |
89 | |
90 /** | |
91 * This hashmap is used internally to cache results of class_exists() | |
92 * look-ups. | |
93 * | |
94 * @var array | |
95 */ | |
96 private $classExists = array(); | |
97 | |
98 /** | |
99 * Whether annotations that have not been imported should be ignored. | |
100 * | |
101 * @var boolean | |
102 */ | |
103 private $ignoreNotImportedAnnotations = false; | |
104 | |
105 /** | |
106 * An array of default namespaces if operating in simple mode. | |
107 * | |
108 * @var array | |
109 */ | |
110 private $namespaces = array(); | |
111 | |
112 /** | |
113 * A list with annotations that are not causing exceptions when not resolved to an annotation class. | |
114 * | |
115 * The names must be the raw names as used in the class, not the fully qualified | |
116 * class names. | |
117 * | |
118 * @var array | |
119 */ | |
120 private $ignoredAnnotationNames = array(); | |
121 | |
122 /** | |
123 * @var string | |
124 */ | |
125 private $context = ''; | |
126 | |
127 /** | |
128 * Hash-map for caching annotation metadata. | |
129 * | |
130 * @var array | |
131 */ | |
132 private static $annotationMetadata = array( | |
133 'Doctrine\Common\Annotations\Annotation\Target' => array( | |
134 'is_annotation' => true, | |
135 'has_constructor' => true, | |
136 'properties' => array(), | |
137 'targets_literal' => 'ANNOTATION_CLASS', | |
138 'targets' => Target::TARGET_CLASS, | |
139 'default_property' => 'value', | |
140 'attribute_types' => array( | |
141 'value' => array( | |
142 'required' => false, | |
143 'type' =>'array', | |
144 'array_type'=>'string', | |
145 'value' =>'array<string>' | |
146 ) | |
147 ), | |
148 ), | |
149 'Doctrine\Common\Annotations\Annotation\Attribute' => array( | |
150 'is_annotation' => true, | |
151 'has_constructor' => false, | |
152 'targets_literal' => 'ANNOTATION_ANNOTATION', | |
153 'targets' => Target::TARGET_ANNOTATION, | |
154 'default_property' => 'name', | |
155 'properties' => array( | |
156 'name' => 'name', | |
157 'type' => 'type', | |
158 'required' => 'required' | |
159 ), | |
160 'attribute_types' => array( | |
161 'value' => array( | |
162 'required' => true, | |
163 'type' =>'string', | |
164 'value' =>'string' | |
165 ), | |
166 'type' => array( | |
167 'required' =>true, | |
168 'type' =>'string', | |
169 'value' =>'string' | |
170 ), | |
171 'required' => array( | |
172 'required' =>false, | |
173 'type' =>'boolean', | |
174 'value' =>'boolean' | |
175 ) | |
176 ), | |
177 ), | |
178 'Doctrine\Common\Annotations\Annotation\Attributes' => array( | |
179 'is_annotation' => true, | |
180 'has_constructor' => false, | |
181 'targets_literal' => 'ANNOTATION_CLASS', | |
182 'targets' => Target::TARGET_CLASS, | |
183 'default_property' => 'value', | |
184 'properties' => array( | |
185 'value' => 'value' | |
186 ), | |
187 'attribute_types' => array( | |
188 'value' => array( | |
189 'type' =>'array', | |
190 'required' =>true, | |
191 'array_type'=>'Doctrine\Common\Annotations\Annotation\Attribute', | |
192 'value' =>'array<Doctrine\Common\Annotations\Annotation\Attribute>' | |
193 ) | |
194 ), | |
195 ), | |
196 'Doctrine\Common\Annotations\Annotation\Enum' => array( | |
197 'is_annotation' => true, | |
198 'has_constructor' => true, | |
199 'targets_literal' => 'ANNOTATION_PROPERTY', | |
200 'targets' => Target::TARGET_PROPERTY, | |
201 'default_property' => 'value', | |
202 'properties' => array( | |
203 'value' => 'value' | |
204 ), | |
205 'attribute_types' => array( | |
206 'value' => array( | |
207 'type' => 'array', | |
208 'required' => true, | |
209 ), | |
210 'literal' => array( | |
211 'type' => 'array', | |
212 'required' => false, | |
213 ), | |
214 ), | |
215 ), | |
216 ); | |
217 | |
218 /** | |
219 * Hash-map for handle types declaration. | |
220 * | |
221 * @var array | |
222 */ | |
223 private static $typeMap = array( | |
224 'float' => 'double', | |
225 'bool' => 'boolean', | |
226 // allow uppercase Boolean in honor of George Boole | |
227 'Boolean' => 'boolean', | |
228 'int' => 'integer', | |
229 ); | |
230 | |
231 /** | |
232 * Constructs a new DocParser. | |
233 */ | |
234 public function __construct() | |
235 { | |
236 $this->lexer = new DocLexer; | |
237 } | |
238 | |
239 /** | |
240 * Sets the annotation names that are ignored during the parsing process. | |
241 * | |
242 * The names are supposed to be the raw names as used in the class, not the | |
243 * fully qualified class names. | |
244 * | |
245 * @param array $names | |
246 * | |
247 * @return void | |
248 */ | |
249 public function setIgnoredAnnotationNames(array $names) | |
250 { | |
251 $this->ignoredAnnotationNames = $names; | |
252 } | |
253 | |
254 /** | |
255 * Sets ignore on not-imported annotations. | |
256 * | |
257 * @param boolean $bool | |
258 * | |
259 * @return void | |
260 */ | |
261 public function setIgnoreNotImportedAnnotations($bool) | |
262 { | |
263 $this->ignoreNotImportedAnnotations = (boolean) $bool; | |
264 } | |
265 | |
266 /** | |
267 * Sets the default namespaces. | |
268 * | |
269 * @param array $namespace | |
270 * | |
271 * @return void | |
272 * | |
273 * @throws \RuntimeException | |
274 */ | |
275 public function addNamespace($namespace) | |
276 { | |
277 if ($this->imports) { | |
278 throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); | |
279 } | |
280 | |
281 $this->namespaces[] = $namespace; | |
282 } | |
283 | |
284 /** | |
285 * Sets the imports. | |
286 * | |
287 * @param array $imports | |
288 * | |
289 * @return void | |
290 * | |
291 * @throws \RuntimeException | |
292 */ | |
293 public function setImports(array $imports) | |
294 { | |
295 if ($this->namespaces) { | |
296 throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); | |
297 } | |
298 | |
299 $this->imports = $imports; | |
300 } | |
301 | |
302 /** | |
303 * Sets current target context as bitmask. | |
304 * | |
305 * @param integer $target | |
306 * | |
307 * @return void | |
308 */ | |
309 public function setTarget($target) | |
310 { | |
311 $this->target = $target; | |
312 } | |
313 | |
314 /** | |
315 * Parses the given docblock string for annotations. | |
316 * | |
317 * @param string $input The docblock string to parse. | |
318 * @param string $context The parsing context. | |
319 * | |
320 * @return array Array of annotations. If no annotations are found, an empty array is returned. | |
321 */ | |
322 public function parse($input, $context = '') | |
323 { | |
324 $pos = $this->findInitialTokenPosition($input); | |
325 if ($pos === null) { | |
326 return array(); | |
327 } | |
328 | |
329 $this->context = $context; | |
330 | |
331 $this->lexer->setInput(trim(substr($input, $pos), '* /')); | |
332 $this->lexer->moveNext(); | |
333 | |
334 return $this->Annotations(); | |
335 } | |
336 | |
337 /** | |
338 * Finds the first valid annotation | |
339 * | |
340 * @param string $input The docblock string to parse | |
341 * | |
342 * @return int|null | |
343 */ | |
344 private function findInitialTokenPosition($input) | |
345 { | |
346 $pos = 0; | |
347 | |
348 // search for first valid annotation | |
349 while (($pos = strpos($input, '@', $pos)) !== false) { | |
350 // if the @ is preceded by a space or * it is valid | |
351 if ($pos === 0 || $input[$pos - 1] === ' ' || $input[$pos - 1] === '*') { | |
352 return $pos; | |
353 } | |
354 | |
355 $pos++; | |
356 } | |
357 | |
358 return null; | |
359 } | |
360 | |
361 /** | |
362 * Attempts to match the given token with the current lookahead token. | |
363 * If they match, updates the lookahead token; otherwise raises a syntax error. | |
364 * | |
365 * @param integer $token Type of token. | |
366 * | |
367 * @return boolean True if tokens match; false otherwise. | |
368 */ | |
369 private function match($token) | |
370 { | |
371 if ( ! $this->lexer->isNextToken($token) ) { | |
372 $this->syntaxError($this->lexer->getLiteral($token)); | |
373 } | |
374 | |
375 return $this->lexer->moveNext(); | |
376 } | |
377 | |
378 /** | |
379 * Attempts to match the current lookahead token with any of the given tokens. | |
380 * | |
381 * If any of them matches, this method updates the lookahead token; otherwise | |
382 * a syntax error is raised. | |
383 * | |
384 * @param array $tokens | |
385 * | |
386 * @return boolean | |
387 */ | |
388 private function matchAny(array $tokens) | |
389 { | |
390 if ( ! $this->lexer->isNextTokenAny($tokens)) { | |
391 $this->syntaxError(implode(' or ', array_map(array($this->lexer, 'getLiteral'), $tokens))); | |
392 } | |
393 | |
394 return $this->lexer->moveNext(); | |
395 } | |
396 | |
397 /** | |
398 * Generates a new syntax error. | |
399 * | |
400 * @param string $expected Expected string. | |
401 * @param array|null $token Optional token. | |
402 * | |
403 * @return void | |
404 * | |
405 * @throws AnnotationException | |
406 */ | |
407 private function syntaxError($expected, $token = null) | |
408 { | |
409 if ($token === null) { | |
410 $token = $this->lexer->lookahead; | |
411 } | |
412 | |
413 $message = sprintf('Expected %s, got ', $expected); | |
414 $message .= ($this->lexer->lookahead === null) | |
415 ? 'end of string' | |
416 : sprintf("'%s' at position %s", $token['value'], $token['position']); | |
417 | |
418 if (strlen($this->context)) { | |
419 $message .= ' in ' . $this->context; | |
420 } | |
421 | |
422 $message .= '.'; | |
423 | |
424 throw AnnotationException::syntaxError($message); | |
425 } | |
426 | |
427 /** | |
428 * Attempts to check if a class exists or not. This never goes through the PHP autoloading mechanism | |
429 * but uses the {@link AnnotationRegistry} to load classes. | |
430 * | |
431 * @param string $fqcn | |
432 * | |
433 * @return boolean | |
434 */ | |
435 private function classExists($fqcn) | |
436 { | |
437 if (isset($this->classExists[$fqcn])) { | |
438 return $this->classExists[$fqcn]; | |
439 } | |
440 | |
441 // first check if the class already exists, maybe loaded through another AnnotationReader | |
442 if (class_exists($fqcn, false)) { | |
443 return $this->classExists[$fqcn] = true; | |
444 } | |
445 | |
446 // final check, does this class exist? | |
447 return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn); | |
448 } | |
449 | |
450 /** | |
451 * Collects parsing metadata for a given annotation class | |
452 * | |
453 * @param string $name The annotation name | |
454 * | |
455 * @return void | |
456 */ | |
457 private function collectAnnotationMetadata($name) | |
458 { | |
459 if (self::$metadataParser === null) { | |
460 self::$metadataParser = new self(); | |
461 | |
462 self::$metadataParser->setIgnoreNotImportedAnnotations(true); | |
463 self::$metadataParser->setIgnoredAnnotationNames($this->ignoredAnnotationNames); | |
464 self::$metadataParser->setImports(array( | |
465 'enum' => 'Doctrine\Common\Annotations\Annotation\Enum', | |
466 'target' => 'Doctrine\Common\Annotations\Annotation\Target', | |
467 'attribute' => 'Doctrine\Common\Annotations\Annotation\Attribute', | |
468 'attributes' => 'Doctrine\Common\Annotations\Annotation\Attributes' | |
469 )); | |
470 | |
471 AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Enum.php'); | |
472 AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Target.php'); | |
473 AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attribute.php'); | |
474 AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attributes.php'); | |
475 } | |
476 | |
477 $class = new \ReflectionClass($name); | |
478 $docComment = $class->getDocComment(); | |
479 | |
480 // Sets default values for annotation metadata | |
481 $metadata = array( | |
482 'default_property' => null, | |
483 'has_constructor' => (null !== $constructor = $class->getConstructor()) && $constructor->getNumberOfParameters() > 0, | |
484 'properties' => array(), | |
485 'property_types' => array(), | |
486 'attribute_types' => array(), | |
487 'targets_literal' => null, | |
488 'targets' => Target::TARGET_ALL, | |
489 'is_annotation' => false !== strpos($docComment, '@Annotation'), | |
490 ); | |
491 | |
492 // verify that the class is really meant to be an annotation | |
493 if ($metadata['is_annotation']) { | |
494 self::$metadataParser->setTarget(Target::TARGET_CLASS); | |
495 | |
496 foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) { | |
497 if ($annotation instanceof Target) { | |
498 $metadata['targets'] = $annotation->targets; | |
499 $metadata['targets_literal'] = $annotation->literal; | |
500 | |
501 continue; | |
502 } | |
503 | |
504 if ($annotation instanceof Attributes) { | |
505 foreach ($annotation->value as $attribute) { | |
506 $this->collectAttributeTypeMetadata($metadata, $attribute); | |
507 } | |
508 } | |
509 } | |
510 | |
511 // if not has a constructor will inject values into public properties | |
512 if (false === $metadata['has_constructor']) { | |
513 // collect all public properties | |
514 foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { | |
515 $metadata['properties'][$property->name] = $property->name; | |
516 | |
517 if (false === ($propertyComment = $property->getDocComment())) { | |
518 continue; | |
519 } | |
520 | |
521 $attribute = new Attribute(); | |
522 | |
523 $attribute->required = (false !== strpos($propertyComment, '@Required')); | |
524 $attribute->name = $property->name; | |
525 $attribute->type = (false !== strpos($propertyComment, '@var') && preg_match('/@var\s+([^\s]+)/',$propertyComment, $matches)) | |
526 ? $matches[1] | |
527 : 'mixed'; | |
528 | |
529 $this->collectAttributeTypeMetadata($metadata, $attribute); | |
530 | |
531 // checks if the property has @Enum | |
532 if (false !== strpos($propertyComment, '@Enum')) { | |
533 $context = 'property ' . $class->name . "::\$" . $property->name; | |
534 | |
535 self::$metadataParser->setTarget(Target::TARGET_PROPERTY); | |
536 | |
537 foreach (self::$metadataParser->parse($propertyComment, $context) as $annotation) { | |
538 if ( ! $annotation instanceof Enum) { | |
539 continue; | |
540 } | |
541 | |
542 $metadata['enum'][$property->name]['value'] = $annotation->value; | |
543 $metadata['enum'][$property->name]['literal'] = ( ! empty($annotation->literal)) | |
544 ? $annotation->literal | |
545 : $annotation->value; | |
546 } | |
547 } | |
548 } | |
549 | |
550 // choose the first property as default property | |
551 $metadata['default_property'] = reset($metadata['properties']); | |
552 } | |
553 } | |
554 | |
555 self::$annotationMetadata[$name] = $metadata; | |
556 } | |
557 | |
558 /** | |
559 * Collects parsing metadata for a given attribute. | |
560 * | |
561 * @param array $metadata | |
562 * @param Attribute $attribute | |
563 * | |
564 * @return void | |
565 */ | |
566 private function collectAttributeTypeMetadata(&$metadata, Attribute $attribute) | |
567 { | |
568 // handle internal type declaration | |
569 $type = isset(self::$typeMap[$attribute->type]) | |
570 ? self::$typeMap[$attribute->type] | |
571 : $attribute->type; | |
572 | |
573 // handle the case if the property type is mixed | |
574 if ('mixed' === $type) { | |
575 return; | |
576 } | |
577 | |
578 // Evaluate type | |
579 switch (true) { | |
580 // Checks if the property has array<type> | |
581 case (false !== $pos = strpos($type, '<')): | |
582 $arrayType = substr($type, $pos + 1, -1); | |
583 $type = 'array'; | |
584 | |
585 if (isset(self::$typeMap[$arrayType])) { | |
586 $arrayType = self::$typeMap[$arrayType]; | |
587 } | |
588 | |
589 $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType; | |
590 break; | |
591 | |
592 // Checks if the property has type[] | |
593 case (false !== $pos = strrpos($type, '[')): | |
594 $arrayType = substr($type, 0, $pos); | |
595 $type = 'array'; | |
596 | |
597 if (isset(self::$typeMap[$arrayType])) { | |
598 $arrayType = self::$typeMap[$arrayType]; | |
599 } | |
600 | |
601 $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType; | |
602 break; | |
603 } | |
604 | |
605 $metadata['attribute_types'][$attribute->name]['type'] = $type; | |
606 $metadata['attribute_types'][$attribute->name]['value'] = $attribute->type; | |
607 $metadata['attribute_types'][$attribute->name]['required'] = $attribute->required; | |
608 } | |
609 | |
610 /** | |
611 * Annotations ::= Annotation {[ "*" ]* [Annotation]}* | |
612 * | |
613 * @return array | |
614 */ | |
615 private function Annotations() | |
616 { | |
617 $annotations = array(); | |
618 | |
619 while (null !== $this->lexer->lookahead) { | |
620 if (DocLexer::T_AT !== $this->lexer->lookahead['type']) { | |
621 $this->lexer->moveNext(); | |
622 continue; | |
623 } | |
624 | |
625 // make sure the @ is preceded by non-catchable pattern | |
626 if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) { | |
627 $this->lexer->moveNext(); | |
628 continue; | |
629 } | |
630 | |
631 // make sure the @ is followed by either a namespace separator, or | |
632 // an identifier token | |
633 if ((null === $peek = $this->lexer->glimpse()) | |
634 || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true)) | |
635 || $peek['position'] !== $this->lexer->lookahead['position'] + 1) { | |
636 $this->lexer->moveNext(); | |
637 continue; | |
638 } | |
639 | |
640 $this->isNestedAnnotation = false; | |
641 if (false !== $annot = $this->Annotation()) { | |
642 $annotations[] = $annot; | |
643 } | |
644 } | |
645 | |
646 return $annotations; | |
647 } | |
648 | |
649 /** | |
650 * Annotation ::= "@" AnnotationName MethodCall | |
651 * AnnotationName ::= QualifiedName | SimpleName | |
652 * QualifiedName ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName | |
653 * NameSpacePart ::= identifier | null | false | true | |
654 * SimpleName ::= identifier | null | false | true | |
655 * | |
656 * @return mixed False if it is not a valid annotation. | |
657 * | |
658 * @throws AnnotationException | |
659 */ | |
660 private function Annotation() | |
661 { | |
662 $this->match(DocLexer::T_AT); | |
663 | |
664 // check if we have an annotation | |
665 $name = $this->Identifier(); | |
666 | |
667 // only process names which are not fully qualified, yet | |
668 // fully qualified names must start with a \ | |
669 $originalName = $name; | |
670 | |
671 if ('\\' !== $name[0]) { | |
672 $alias = (false === $pos = strpos($name, '\\'))? $name : substr($name, 0, $pos); | |
673 $found = false; | |
674 | |
675 if ($this->namespaces) { | |
676 foreach ($this->namespaces as $namespace) { | |
677 if ($this->classExists($namespace.'\\'.$name)) { | |
678 $name = $namespace.'\\'.$name; | |
679 $found = true; | |
680 break; | |
681 } | |
682 } | |
683 } elseif (isset($this->imports[$loweredAlias = strtolower($alias)])) { | |
684 $found = true; | |
685 $name = (false !== $pos) | |
686 ? $this->imports[$loweredAlias] . substr($name, $pos) | |
687 : $this->imports[$loweredAlias]; | |
688 } elseif ( ! isset($this->ignoredAnnotationNames[$name]) | |
689 && isset($this->imports['__NAMESPACE__']) | |
690 && $this->classExists($this->imports['__NAMESPACE__'] . '\\' . $name) | |
691 ) { | |
692 $name = $this->imports['__NAMESPACE__'].'\\'.$name; | |
693 $found = true; | |
694 } elseif (! isset($this->ignoredAnnotationNames[$name]) && $this->classExists($name)) { | |
695 $found = true; | |
696 } | |
697 | |
698 if ( ! $found) { | |
699 if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) { | |
700 return false; | |
701 } | |
702 | |
703 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)); | |
704 } | |
705 } | |
706 | |
707 if ( ! $this->classExists($name)) { | |
708 throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context)); | |
709 } | |
710 | |
711 // at this point, $name contains the fully qualified class name of the | |
712 // annotation, and it is also guaranteed that this class exists, and | |
713 // that it is loaded | |
714 | |
715 | |
716 // collects the metadata annotation only if there is not yet | |
717 if ( ! isset(self::$annotationMetadata[$name])) { | |
718 $this->collectAnnotationMetadata($name); | |
719 } | |
720 | |
721 // verify that the class is really meant to be an annotation and not just any ordinary class | |
722 if (self::$annotationMetadata[$name]['is_annotation'] === false) { | |
723 if (isset($this->ignoredAnnotationNames[$originalName])) { | |
724 return false; | |
725 } | |
726 | |
727 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)); | |
728 } | |
729 | |
730 //if target is nested annotation | |
731 $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target; | |
732 | |
733 // Next will be nested | |
734 $this->isNestedAnnotation = true; | |
735 | |
736 //if annotation does not support current target | |
737 if (0 === (self::$annotationMetadata[$name]['targets'] & $target) && $target) { | |
738 throw AnnotationException::semanticalError( | |
739 sprintf('Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.', | |
740 $originalName, $this->context, self::$annotationMetadata[$name]['targets_literal']) | |
741 ); | |
742 } | |
743 | |
744 $values = $this->MethodCall(); | |
745 | |
746 if (isset(self::$annotationMetadata[$name]['enum'])) { | |
747 // checks all declared attributes | |
748 foreach (self::$annotationMetadata[$name]['enum'] as $property => $enum) { | |
749 // checks if the attribute is a valid enumerator | |
750 if (isset($values[$property]) && ! in_array($values[$property], $enum['value'])) { | |
751 throw AnnotationException::enumeratorError($property, $name, $this->context, $enum['literal'], $values[$property]); | |
752 } | |
753 } | |
754 } | |
755 | |
756 // checks all declared attributes | |
757 foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) { | |
758 if ($property === self::$annotationMetadata[$name]['default_property'] | |
759 && !isset($values[$property]) && isset($values['value'])) { | |
760 $property = 'value'; | |
761 } | |
762 | |
763 // handle a not given attribute or null value | |
764 if (!isset($values[$property])) { | |
765 if ($type['required']) { | |
766 throw AnnotationException::requiredError($property, $originalName, $this->context, 'a(n) '.$type['value']); | |
767 } | |
768 | |
769 continue; | |
770 } | |
771 | |
772 if ($type['type'] === 'array') { | |
773 // handle the case of a single value | |
774 if ( ! is_array($values[$property])) { | |
775 $values[$property] = array($values[$property]); | |
776 } | |
777 | |
778 // checks if the attribute has array type declaration, such as "array<string>" | |
779 if (isset($type['array_type'])) { | |
780 foreach ($values[$property] as $item) { | |
781 if (gettype($item) !== $type['array_type'] && !$item instanceof $type['array_type']) { | |
782 throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'either a(n) '.$type['array_type'].', or an array of '.$type['array_type'].'s', $item); | |
783 } | |
784 } | |
785 } | |
786 } elseif (gettype($values[$property]) !== $type['type'] && !$values[$property] instanceof $type['type']) { | |
787 throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'a(n) '.$type['value'], $values[$property]); | |
788 } | |
789 } | |
790 | |
791 // check if the annotation expects values via the constructor, | |
792 // or directly injected into public properties | |
793 if (self::$annotationMetadata[$name]['has_constructor'] === true) { | |
794 return new $name($values); | |
795 } | |
796 | |
797 $instance = new $name(); | |
798 | |
799 foreach ($values as $property => $value) { | |
800 if (!isset(self::$annotationMetadata[$name]['properties'][$property])) { | |
801 if ('value' !== $property) { | |
802 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']))); | |
803 } | |
804 | |
805 // handle the case if the property has no annotations | |
806 if ( ! $property = self::$annotationMetadata[$name]['default_property']) { | |
807 throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not accept any values, but got %s.', $originalName, $this->context, json_encode($values))); | |
808 } | |
809 } | |
810 | |
811 $instance->{$property} = $value; | |
812 } | |
813 | |
814 return $instance; | |
815 } | |
816 | |
817 /** | |
818 * MethodCall ::= ["(" [Values] ")"] | |
819 * | |
820 * @return array | |
821 */ | |
822 private function MethodCall() | |
823 { | |
824 $values = array(); | |
825 | |
826 if ( ! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) { | |
827 return $values; | |
828 } | |
829 | |
830 $this->match(DocLexer::T_OPEN_PARENTHESIS); | |
831 | |
832 if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) { | |
833 $values = $this->Values(); | |
834 } | |
835 | |
836 $this->match(DocLexer::T_CLOSE_PARENTHESIS); | |
837 | |
838 return $values; | |
839 } | |
840 | |
841 /** | |
842 * Values ::= Array | Value {"," Value}* [","] | |
843 * | |
844 * @return array | |
845 */ | |
846 private function Values() | |
847 { | |
848 $values = array($this->Value()); | |
849 | |
850 while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { | |
851 $this->match(DocLexer::T_COMMA); | |
852 | |
853 if ($this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) { | |
854 break; | |
855 } | |
856 | |
857 $token = $this->lexer->lookahead; | |
858 $value = $this->Value(); | |
859 | |
860 if ( ! is_object($value) && ! is_array($value)) { | |
861 $this->syntaxError('Value', $token); | |
862 } | |
863 | |
864 $values[] = $value; | |
865 } | |
866 | |
867 foreach ($values as $k => $value) { | |
868 if (is_object($value) && $value instanceof \stdClass) { | |
869 $values[$value->name] = $value->value; | |
870 } else if ( ! isset($values['value'])){ | |
871 $values['value'] = $value; | |
872 } else { | |
873 if ( ! is_array($values['value'])) { | |
874 $values['value'] = array($values['value']); | |
875 } | |
876 | |
877 $values['value'][] = $value; | |
878 } | |
879 | |
880 unset($values[$k]); | |
881 } | |
882 | |
883 return $values; | |
884 } | |
885 | |
886 /** | |
887 * Constant ::= integer | string | float | boolean | |
888 * | |
889 * @return mixed | |
890 * | |
891 * @throws AnnotationException | |
892 */ | |
893 private function Constant() | |
894 { | |
895 $identifier = $this->Identifier(); | |
896 | |
897 if ( ! defined($identifier) && false !== strpos($identifier, '::') && '\\' !== $identifier[0]) { | |
898 list($className, $const) = explode('::', $identifier); | |
899 | |
900 $alias = (false === $pos = strpos($className, '\\')) ? $className : substr($className, 0, $pos); | |
901 $found = false; | |
902 | |
903 switch (true) { | |
904 case !empty ($this->namespaces): | |
905 foreach ($this->namespaces as $ns) { | |
906 if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) { | |
907 $className = $ns.'\\'.$className; | |
908 $found = true; | |
909 break; | |
910 } | |
911 } | |
912 break; | |
913 | |
914 case isset($this->imports[$loweredAlias = strtolower($alias)]): | |
915 $found = true; | |
916 $className = (false !== $pos) | |
917 ? $this->imports[$loweredAlias] . substr($className, $pos) | |
918 : $this->imports[$loweredAlias]; | |
919 break; | |
920 | |
921 default: | |
922 if(isset($this->imports['__NAMESPACE__'])) { | |
923 $ns = $this->imports['__NAMESPACE__']; | |
924 | |
925 if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) { | |
926 $className = $ns.'\\'.$className; | |
927 $found = true; | |
928 } | |
929 } | |
930 break; | |
931 } | |
932 | |
933 if ($found) { | |
934 $identifier = $className . '::' . $const; | |
935 } | |
936 } | |
937 | |
938 // checks if identifier ends with ::class, \strlen('::class') === 7 | |
939 $classPos = stripos($identifier, '::class'); | |
940 if ($classPos === strlen($identifier) - 7) { | |
941 return substr($identifier, 0, $classPos); | |
942 } | |
943 | |
944 if (!defined($identifier)) { | |
945 throw AnnotationException::semanticalErrorConstants($identifier, $this->context); | |
946 } | |
947 | |
948 return constant($identifier); | |
949 } | |
950 | |
951 /** | |
952 * Identifier ::= string | |
953 * | |
954 * @return string | |
955 */ | |
956 private function Identifier() | |
957 { | |
958 // check if we have an annotation | |
959 if ( ! $this->lexer->isNextTokenAny(self::$classIdentifiers)) { | |
960 $this->syntaxError('namespace separator or identifier'); | |
961 } | |
962 | |
963 $this->lexer->moveNext(); | |
964 | |
965 $className = $this->lexer->token['value']; | |
966 | |
967 while ($this->lexer->lookahead['position'] === ($this->lexer->token['position'] + strlen($this->lexer->token['value'])) | |
968 && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) { | |
969 | |
970 $this->match(DocLexer::T_NAMESPACE_SEPARATOR); | |
971 $this->matchAny(self::$classIdentifiers); | |
972 | |
973 $className .= '\\' . $this->lexer->token['value']; | |
974 } | |
975 | |
976 return $className; | |
977 } | |
978 | |
979 /** | |
980 * Value ::= PlainValue | FieldAssignment | |
981 * | |
982 * @return mixed | |
983 */ | |
984 private function Value() | |
985 { | |
986 $peek = $this->lexer->glimpse(); | |
987 | |
988 if (DocLexer::T_EQUALS === $peek['type']) { | |
989 return $this->FieldAssignment(); | |
990 } | |
991 | |
992 return $this->PlainValue(); | |
993 } | |
994 | |
995 /** | |
996 * PlainValue ::= integer | string | float | boolean | Array | Annotation | |
997 * | |
998 * @return mixed | |
999 */ | |
1000 private function PlainValue() | |
1001 { | |
1002 if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) { | |
1003 return $this->Arrayx(); | |
1004 } | |
1005 | |
1006 if ($this->lexer->isNextToken(DocLexer::T_AT)) { | |
1007 return $this->Annotation(); | |
1008 } | |
1009 | |
1010 if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) { | |
1011 return $this->Constant(); | |
1012 } | |
1013 | |
1014 switch ($this->lexer->lookahead['type']) { | |
1015 case DocLexer::T_STRING: | |
1016 $this->match(DocLexer::T_STRING); | |
1017 return $this->lexer->token['value']; | |
1018 | |
1019 case DocLexer::T_INTEGER: | |
1020 $this->match(DocLexer::T_INTEGER); | |
1021 return (int)$this->lexer->token['value']; | |
1022 | |
1023 case DocLexer::T_FLOAT: | |
1024 $this->match(DocLexer::T_FLOAT); | |
1025 return (float)$this->lexer->token['value']; | |
1026 | |
1027 case DocLexer::T_TRUE: | |
1028 $this->match(DocLexer::T_TRUE); | |
1029 return true; | |
1030 | |
1031 case DocLexer::T_FALSE: | |
1032 $this->match(DocLexer::T_FALSE); | |
1033 return false; | |
1034 | |
1035 case DocLexer::T_NULL: | |
1036 $this->match(DocLexer::T_NULL); | |
1037 return null; | |
1038 | |
1039 default: | |
1040 $this->syntaxError('PlainValue'); | |
1041 } | |
1042 } | |
1043 | |
1044 /** | |
1045 * FieldAssignment ::= FieldName "=" PlainValue | |
1046 * FieldName ::= identifier | |
1047 * | |
1048 * @return array | |
1049 */ | |
1050 private function FieldAssignment() | |
1051 { | |
1052 $this->match(DocLexer::T_IDENTIFIER); | |
1053 $fieldName = $this->lexer->token['value']; | |
1054 | |
1055 $this->match(DocLexer::T_EQUALS); | |
1056 | |
1057 $item = new \stdClass(); | |
1058 $item->name = $fieldName; | |
1059 $item->value = $this->PlainValue(); | |
1060 | |
1061 return $item; | |
1062 } | |
1063 | |
1064 /** | |
1065 * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}" | |
1066 * | |
1067 * @return array | |
1068 */ | |
1069 private function Arrayx() | |
1070 { | |
1071 $array = $values = array(); | |
1072 | |
1073 $this->match(DocLexer::T_OPEN_CURLY_BRACES); | |
1074 | |
1075 // If the array is empty, stop parsing and return. | |
1076 if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) { | |
1077 $this->match(DocLexer::T_CLOSE_CURLY_BRACES); | |
1078 | |
1079 return $array; | |
1080 } | |
1081 | |
1082 $values[] = $this->ArrayEntry(); | |
1083 | |
1084 while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { | |
1085 $this->match(DocLexer::T_COMMA); | |
1086 | |
1087 // optional trailing comma | |
1088 if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) { | |
1089 break; | |
1090 } | |
1091 | |
1092 $values[] = $this->ArrayEntry(); | |
1093 } | |
1094 | |
1095 $this->match(DocLexer::T_CLOSE_CURLY_BRACES); | |
1096 | |
1097 foreach ($values as $value) { | |
1098 list ($key, $val) = $value; | |
1099 | |
1100 if ($key !== null) { | |
1101 $array[$key] = $val; | |
1102 } else { | |
1103 $array[] = $val; | |
1104 } | |
1105 } | |
1106 | |
1107 return $array; | |
1108 } | |
1109 | |
1110 /** | |
1111 * ArrayEntry ::= Value | KeyValuePair | |
1112 * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant | |
1113 * Key ::= string | integer | Constant | |
1114 * | |
1115 * @return array | |
1116 */ | |
1117 private function ArrayEntry() | |
1118 { | |
1119 $peek = $this->lexer->glimpse(); | |
1120 | |
1121 if (DocLexer::T_EQUALS === $peek['type'] | |
1122 || DocLexer::T_COLON === $peek['type']) { | |
1123 | |
1124 if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) { | |
1125 $key = $this->Constant(); | |
1126 } else { | |
1127 $this->matchAny(array(DocLexer::T_INTEGER, DocLexer::T_STRING)); | |
1128 $key = $this->lexer->token['value']; | |
1129 } | |
1130 | |
1131 $this->matchAny(array(DocLexer::T_EQUALS, DocLexer::T_COLON)); | |
1132 | |
1133 return array($key, $this->PlainValue()); | |
1134 } | |
1135 | |
1136 return array(null, $this->Value()); | |
1137 } | |
1138 } |