annotate vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocParser.php @ 19:fa3358dc1485 tip

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