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