annotate vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocParser.php @ 7:848c88cfe644

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