Chris@0: . Chris@0: */ Chris@0: Chris@0: namespace Doctrine\Common\Annotations; Chris@0: Chris@0: use Doctrine\Common\Annotations\Annotation\Attribute; Chris@0: use ReflectionClass; Chris@0: use Doctrine\Common\Annotations\Annotation\Enum; Chris@0: use Doctrine\Common\Annotations\Annotation\Target; Chris@0: use Doctrine\Common\Annotations\Annotation\Attributes; Chris@0: Chris@0: /** Chris@0: * A parser for docblock annotations. Chris@0: * Chris@0: * It is strongly discouraged to change the default annotation parsing process. Chris@0: * Chris@0: * @author Benjamin Eberlei Chris@0: * @author Guilherme Blanco Chris@0: * @author Jonathan Wage Chris@0: * @author Roman Borschel Chris@0: * @author Johannes M. Schmitt Chris@0: * @author Fabio B. Silva Chris@0: */ Chris@0: final class DocParser Chris@0: { Chris@0: /** Chris@0: * An array of all valid tokens for a class name. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: private static $classIdentifiers = array( Chris@0: DocLexer::T_IDENTIFIER, Chris@0: DocLexer::T_TRUE, Chris@0: DocLexer::T_FALSE, Chris@0: DocLexer::T_NULL Chris@0: ); Chris@0: Chris@0: /** Chris@0: * The lexer. Chris@0: * Chris@0: * @var \Doctrine\Common\Annotations\DocLexer Chris@0: */ Chris@0: private $lexer; Chris@0: Chris@0: /** Chris@0: * Current target context. Chris@0: * Chris@12: * @var integer Chris@0: */ Chris@0: private $target; Chris@0: Chris@0: /** Chris@0: * Doc parser used to collect annotation target. Chris@0: * Chris@0: * @var \Doctrine\Common\Annotations\DocParser Chris@0: */ Chris@0: private static $metadataParser; Chris@0: Chris@0: /** Chris@0: * Flag to control if the current annotation is nested or not. Chris@0: * Chris@0: * @var boolean Chris@0: */ Chris@0: private $isNestedAnnotation = false; Chris@0: Chris@0: /** Chris@0: * Hashmap containing all use-statements that are to be used when parsing Chris@0: * the given doc block. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: private $imports = array(); Chris@0: Chris@0: /** Chris@0: * This hashmap is used internally to cache results of class_exists() Chris@0: * look-ups. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: private $classExists = array(); Chris@0: Chris@0: /** Chris@0: * Whether annotations that have not been imported should be ignored. Chris@0: * Chris@0: * @var boolean Chris@0: */ Chris@0: private $ignoreNotImportedAnnotations = false; Chris@0: Chris@0: /** Chris@0: * An array of default namespaces if operating in simple mode. Chris@0: * Chris@12: * @var string[] Chris@0: */ Chris@0: private $namespaces = array(); Chris@0: Chris@0: /** Chris@0: * A list with annotations that are not causing exceptions when not resolved to an annotation class. Chris@0: * Chris@0: * The names must be the raw names as used in the class, not the fully qualified Chris@0: * class names. Chris@0: * Chris@12: * @var bool[] indexed by annotation name Chris@0: */ Chris@0: private $ignoredAnnotationNames = array(); Chris@0: Chris@0: /** Chris@12: * A list with annotations in namespaced format Chris@12: * that are not causing exceptions when not resolved to an annotation class. Chris@12: * Chris@12: * @var bool[] indexed by namespace name Chris@12: */ Chris@12: private $ignoredAnnotationNamespaces = array(); Chris@12: Chris@12: /** Chris@0: * @var string Chris@0: */ Chris@0: private $context = ''; Chris@0: Chris@0: /** Chris@0: * Hash-map for caching annotation metadata. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: private static $annotationMetadata = array( Chris@0: 'Doctrine\Common\Annotations\Annotation\Target' => array( Chris@0: 'is_annotation' => true, Chris@0: 'has_constructor' => true, Chris@0: 'properties' => array(), Chris@0: 'targets_literal' => 'ANNOTATION_CLASS', Chris@0: 'targets' => Target::TARGET_CLASS, Chris@0: 'default_property' => 'value', Chris@0: 'attribute_types' => array( Chris@0: 'value' => array( Chris@0: 'required' => false, Chris@0: 'type' =>'array', Chris@0: 'array_type'=>'string', Chris@0: 'value' =>'array' Chris@0: ) Chris@0: ), Chris@0: ), Chris@0: 'Doctrine\Common\Annotations\Annotation\Attribute' => array( Chris@0: 'is_annotation' => true, Chris@0: 'has_constructor' => false, Chris@0: 'targets_literal' => 'ANNOTATION_ANNOTATION', Chris@0: 'targets' => Target::TARGET_ANNOTATION, Chris@0: 'default_property' => 'name', Chris@0: 'properties' => array( Chris@0: 'name' => 'name', Chris@0: 'type' => 'type', Chris@0: 'required' => 'required' Chris@0: ), Chris@0: 'attribute_types' => array( Chris@0: 'value' => array( Chris@0: 'required' => true, Chris@0: 'type' =>'string', Chris@0: 'value' =>'string' Chris@0: ), Chris@0: 'type' => array( Chris@0: 'required' =>true, Chris@0: 'type' =>'string', Chris@0: 'value' =>'string' Chris@0: ), Chris@0: 'required' => array( Chris@0: 'required' =>false, Chris@0: 'type' =>'boolean', Chris@0: 'value' =>'boolean' Chris@0: ) Chris@0: ), Chris@0: ), Chris@0: 'Doctrine\Common\Annotations\Annotation\Attributes' => array( Chris@0: 'is_annotation' => true, Chris@0: 'has_constructor' => false, Chris@0: 'targets_literal' => 'ANNOTATION_CLASS', Chris@0: 'targets' => Target::TARGET_CLASS, Chris@0: 'default_property' => 'value', Chris@0: 'properties' => array( Chris@0: 'value' => 'value' Chris@0: ), Chris@0: 'attribute_types' => array( Chris@0: 'value' => array( Chris@0: 'type' =>'array', Chris@0: 'required' =>true, Chris@0: 'array_type'=>'Doctrine\Common\Annotations\Annotation\Attribute', Chris@0: 'value' =>'array' Chris@0: ) Chris@0: ), Chris@0: ), Chris@0: 'Doctrine\Common\Annotations\Annotation\Enum' => array( Chris@0: 'is_annotation' => true, Chris@0: 'has_constructor' => true, Chris@0: 'targets_literal' => 'ANNOTATION_PROPERTY', Chris@0: 'targets' => Target::TARGET_PROPERTY, Chris@0: 'default_property' => 'value', Chris@0: 'properties' => array( Chris@0: 'value' => 'value' Chris@0: ), Chris@0: 'attribute_types' => array( Chris@0: 'value' => array( Chris@0: 'type' => 'array', Chris@0: 'required' => true, Chris@0: ), Chris@0: 'literal' => array( Chris@0: 'type' => 'array', Chris@0: 'required' => false, Chris@0: ), Chris@0: ), Chris@0: ), Chris@0: ); Chris@0: Chris@0: /** Chris@0: * Hash-map for handle types declaration. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: private static $typeMap = array( Chris@0: 'float' => 'double', Chris@0: 'bool' => 'boolean', Chris@0: // allow uppercase Boolean in honor of George Boole Chris@0: 'Boolean' => 'boolean', Chris@0: 'int' => 'integer', Chris@0: ); Chris@0: Chris@0: /** Chris@0: * Constructs a new DocParser. Chris@0: */ Chris@0: public function __construct() Chris@0: { Chris@0: $this->lexer = new DocLexer; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the annotation names that are ignored during the parsing process. Chris@0: * Chris@0: * The names are supposed to be the raw names as used in the class, not the Chris@0: * fully qualified class names. Chris@0: * Chris@12: * @param bool[] $names indexed by annotation name Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: public function setIgnoredAnnotationNames(array $names) Chris@0: { Chris@0: $this->ignoredAnnotationNames = $names; Chris@0: } Chris@0: Chris@0: /** Chris@12: * Sets the annotation namespaces that are ignored during the parsing process. Chris@12: * Chris@12: * @param bool[] $ignoredAnnotationNamespaces indexed by annotation namespace name Chris@12: * Chris@12: * @return void Chris@12: */ Chris@12: public function setIgnoredAnnotationNamespaces($ignoredAnnotationNamespaces) Chris@12: { Chris@12: $this->ignoredAnnotationNamespaces = $ignoredAnnotationNamespaces; Chris@12: } Chris@12: Chris@12: /** Chris@0: * Sets ignore on not-imported annotations. Chris@0: * Chris@0: * @param boolean $bool Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: public function setIgnoreNotImportedAnnotations($bool) Chris@0: { Chris@0: $this->ignoreNotImportedAnnotations = (boolean) $bool; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the default namespaces. Chris@0: * Chris@12: * @param string $namespace Chris@0: * Chris@0: * @return void Chris@0: * Chris@0: * @throws \RuntimeException Chris@0: */ Chris@0: public function addNamespace($namespace) Chris@0: { Chris@0: if ($this->imports) { Chris@0: throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); Chris@0: } Chris@0: Chris@0: $this->namespaces[] = $namespace; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the imports. Chris@0: * Chris@0: * @param array $imports Chris@0: * Chris@0: * @return void Chris@0: * Chris@0: * @throws \RuntimeException Chris@0: */ Chris@0: public function setImports(array $imports) Chris@0: { Chris@0: if ($this->namespaces) { Chris@0: throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); Chris@0: } Chris@0: Chris@0: $this->imports = $imports; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets current target context as bitmask. Chris@0: * Chris@0: * @param integer $target Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: public function setTarget($target) Chris@0: { Chris@0: $this->target = $target; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses the given docblock string for annotations. Chris@0: * Chris@0: * @param string $input The docblock string to parse. Chris@0: * @param string $context The parsing context. Chris@0: * Chris@0: * @return array Array of annotations. If no annotations are found, an empty array is returned. Chris@0: */ Chris@0: public function parse($input, $context = '') Chris@0: { Chris@0: $pos = $this->findInitialTokenPosition($input); Chris@0: if ($pos === null) { Chris@0: return array(); Chris@0: } Chris@0: Chris@0: $this->context = $context; Chris@0: Chris@0: $this->lexer->setInput(trim(substr($input, $pos), '* /')); Chris@0: $this->lexer->moveNext(); Chris@0: Chris@0: return $this->Annotations(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Finds the first valid annotation Chris@0: * Chris@0: * @param string $input The docblock string to parse Chris@0: * Chris@0: * @return int|null Chris@0: */ Chris@0: private function findInitialTokenPosition($input) Chris@0: { Chris@0: $pos = 0; Chris@0: Chris@0: // search for first valid annotation Chris@0: while (($pos = strpos($input, '@', $pos)) !== false) { Chris@12: $preceding = substr($input, $pos - 1, 1); Chris@12: Chris@12: // if the @ is preceded by a space, a tab or * it is valid Chris@12: if ($pos === 0 || $preceding === ' ' || $preceding === '*' || $preceding === "\t") { Chris@0: return $pos; Chris@0: } Chris@0: Chris@0: $pos++; Chris@0: } Chris@0: Chris@0: return null; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Attempts to match the given token with the current lookahead token. Chris@0: * If they match, updates the lookahead token; otherwise raises a syntax error. Chris@0: * Chris@0: * @param integer $token Type of token. Chris@0: * Chris@0: * @return boolean True if tokens match; false otherwise. Chris@0: */ Chris@0: private function match($token) Chris@0: { Chris@0: if ( ! $this->lexer->isNextToken($token) ) { Chris@0: $this->syntaxError($this->lexer->getLiteral($token)); Chris@0: } Chris@0: Chris@0: return $this->lexer->moveNext(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Attempts to match the current lookahead token with any of the given tokens. Chris@0: * Chris@0: * If any of them matches, this method updates the lookahead token; otherwise Chris@0: * a syntax error is raised. Chris@0: * Chris@0: * @param array $tokens Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: private function matchAny(array $tokens) Chris@0: { Chris@0: if ( ! $this->lexer->isNextTokenAny($tokens)) { Chris@0: $this->syntaxError(implode(' or ', array_map(array($this->lexer, 'getLiteral'), $tokens))); Chris@0: } Chris@0: Chris@0: return $this->lexer->moveNext(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Generates a new syntax error. Chris@0: * Chris@0: * @param string $expected Expected string. Chris@0: * @param array|null $token Optional token. Chris@0: * Chris@0: * @return void Chris@0: * Chris@0: * @throws AnnotationException Chris@0: */ Chris@0: private function syntaxError($expected, $token = null) Chris@0: { Chris@0: if ($token === null) { Chris@0: $token = $this->lexer->lookahead; Chris@0: } Chris@0: Chris@0: $message = sprintf('Expected %s, got ', $expected); Chris@0: $message .= ($this->lexer->lookahead === null) Chris@0: ? 'end of string' Chris@0: : sprintf("'%s' at position %s", $token['value'], $token['position']); Chris@0: Chris@0: if (strlen($this->context)) { Chris@0: $message .= ' in ' . $this->context; Chris@0: } Chris@0: Chris@0: $message .= '.'; Chris@0: Chris@0: throw AnnotationException::syntaxError($message); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Attempts to check if a class exists or not. This never goes through the PHP autoloading mechanism Chris@0: * but uses the {@link AnnotationRegistry} to load classes. Chris@0: * Chris@0: * @param string $fqcn Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: private function classExists($fqcn) Chris@0: { Chris@0: if (isset($this->classExists[$fqcn])) { Chris@0: return $this->classExists[$fqcn]; Chris@0: } Chris@0: Chris@0: // first check if the class already exists, maybe loaded through another AnnotationReader Chris@0: if (class_exists($fqcn, false)) { Chris@0: return $this->classExists[$fqcn] = true; Chris@0: } Chris@0: Chris@0: // final check, does this class exist? Chris@0: return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Collects parsing metadata for a given annotation class Chris@0: * Chris@0: * @param string $name The annotation name Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: private function collectAnnotationMetadata($name) Chris@0: { Chris@0: if (self::$metadataParser === null) { Chris@0: self::$metadataParser = new self(); Chris@0: Chris@0: self::$metadataParser->setIgnoreNotImportedAnnotations(true); Chris@0: self::$metadataParser->setIgnoredAnnotationNames($this->ignoredAnnotationNames); Chris@0: self::$metadataParser->setImports(array( Chris@0: 'enum' => 'Doctrine\Common\Annotations\Annotation\Enum', Chris@0: 'target' => 'Doctrine\Common\Annotations\Annotation\Target', Chris@0: 'attribute' => 'Doctrine\Common\Annotations\Annotation\Attribute', Chris@0: 'attributes' => 'Doctrine\Common\Annotations\Annotation\Attributes' Chris@0: )); Chris@0: Chris@0: AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Enum.php'); Chris@0: AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Target.php'); Chris@0: AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attribute.php'); Chris@0: AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attributes.php'); Chris@0: } Chris@0: Chris@0: $class = new \ReflectionClass($name); Chris@0: $docComment = $class->getDocComment(); Chris@0: Chris@0: // Sets default values for annotation metadata Chris@0: $metadata = array( Chris@0: 'default_property' => null, Chris@0: 'has_constructor' => (null !== $constructor = $class->getConstructor()) && $constructor->getNumberOfParameters() > 0, Chris@0: 'properties' => array(), Chris@0: 'property_types' => array(), Chris@0: 'attribute_types' => array(), Chris@0: 'targets_literal' => null, Chris@0: 'targets' => Target::TARGET_ALL, Chris@0: 'is_annotation' => false !== strpos($docComment, '@Annotation'), Chris@0: ); Chris@0: Chris@0: // verify that the class is really meant to be an annotation Chris@0: if ($metadata['is_annotation']) { Chris@0: self::$metadataParser->setTarget(Target::TARGET_CLASS); Chris@0: Chris@0: foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) { Chris@0: if ($annotation instanceof Target) { Chris@0: $metadata['targets'] = $annotation->targets; Chris@0: $metadata['targets_literal'] = $annotation->literal; Chris@0: Chris@0: continue; Chris@0: } Chris@0: Chris@0: if ($annotation instanceof Attributes) { Chris@0: foreach ($annotation->value as $attribute) { Chris@0: $this->collectAttributeTypeMetadata($metadata, $attribute); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // if not has a constructor will inject values into public properties Chris@0: if (false === $metadata['has_constructor']) { Chris@0: // collect all public properties Chris@0: foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { Chris@0: $metadata['properties'][$property->name] = $property->name; Chris@0: Chris@0: if (false === ($propertyComment = $property->getDocComment())) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: $attribute = new Attribute(); Chris@0: Chris@0: $attribute->required = (false !== strpos($propertyComment, '@Required')); Chris@0: $attribute->name = $property->name; Chris@0: $attribute->type = (false !== strpos($propertyComment, '@var') && preg_match('/@var\s+([^\s]+)/',$propertyComment, $matches)) Chris@0: ? $matches[1] Chris@0: : 'mixed'; Chris@0: Chris@0: $this->collectAttributeTypeMetadata($metadata, $attribute); Chris@0: Chris@0: // checks if the property has @Enum Chris@0: if (false !== strpos($propertyComment, '@Enum')) { Chris@0: $context = 'property ' . $class->name . "::\$" . $property->name; Chris@0: Chris@0: self::$metadataParser->setTarget(Target::TARGET_PROPERTY); Chris@0: Chris@0: foreach (self::$metadataParser->parse($propertyComment, $context) as $annotation) { Chris@0: if ( ! $annotation instanceof Enum) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: $metadata['enum'][$property->name]['value'] = $annotation->value; Chris@0: $metadata['enum'][$property->name]['literal'] = ( ! empty($annotation->literal)) Chris@0: ? $annotation->literal Chris@0: : $annotation->value; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // choose the first property as default property Chris@0: $metadata['default_property'] = reset($metadata['properties']); Chris@0: } Chris@0: } Chris@0: Chris@0: self::$annotationMetadata[$name] = $metadata; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Collects parsing metadata for a given attribute. Chris@0: * Chris@0: * @param array $metadata Chris@0: * @param Attribute $attribute Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: private function collectAttributeTypeMetadata(&$metadata, Attribute $attribute) Chris@0: { Chris@0: // handle internal type declaration Chris@0: $type = isset(self::$typeMap[$attribute->type]) Chris@0: ? self::$typeMap[$attribute->type] Chris@0: : $attribute->type; Chris@0: Chris@0: // handle the case if the property type is mixed Chris@0: if ('mixed' === $type) { Chris@0: return; Chris@0: } Chris@0: Chris@0: // Evaluate type Chris@0: switch (true) { Chris@0: // Checks if the property has array Chris@0: case (false !== $pos = strpos($type, '<')): Chris@0: $arrayType = substr($type, $pos + 1, -1); Chris@0: $type = 'array'; Chris@0: Chris@0: if (isset(self::$typeMap[$arrayType])) { Chris@0: $arrayType = self::$typeMap[$arrayType]; Chris@0: } Chris@0: Chris@0: $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType; Chris@0: break; Chris@0: Chris@0: // Checks if the property has type[] Chris@0: case (false !== $pos = strrpos($type, '[')): Chris@0: $arrayType = substr($type, 0, $pos); Chris@0: $type = 'array'; Chris@0: Chris@0: if (isset(self::$typeMap[$arrayType])) { Chris@0: $arrayType = self::$typeMap[$arrayType]; Chris@0: } Chris@0: Chris@0: $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType; Chris@0: break; Chris@0: } Chris@0: Chris@0: $metadata['attribute_types'][$attribute->name]['type'] = $type; Chris@0: $metadata['attribute_types'][$attribute->name]['value'] = $attribute->type; Chris@0: $metadata['attribute_types'][$attribute->name]['required'] = $attribute->required; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Annotations ::= Annotation {[ "*" ]* [Annotation]}* Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function Annotations() Chris@0: { Chris@0: $annotations = array(); Chris@0: Chris@0: while (null !== $this->lexer->lookahead) { Chris@0: if (DocLexer::T_AT !== $this->lexer->lookahead['type']) { Chris@0: $this->lexer->moveNext(); Chris@0: continue; Chris@0: } Chris@0: Chris@0: // make sure the @ is preceded by non-catchable pattern Chris@0: if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) { Chris@0: $this->lexer->moveNext(); Chris@0: continue; Chris@0: } Chris@0: Chris@0: // make sure the @ is followed by either a namespace separator, or Chris@0: // an identifier token Chris@0: if ((null === $peek = $this->lexer->glimpse()) Chris@0: || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true)) Chris@0: || $peek['position'] !== $this->lexer->lookahead['position'] + 1) { Chris@0: $this->lexer->moveNext(); Chris@0: continue; Chris@0: } Chris@0: Chris@0: $this->isNestedAnnotation = false; Chris@0: if (false !== $annot = $this->Annotation()) { Chris@0: $annotations[] = $annot; Chris@0: } Chris@0: } Chris@0: Chris@0: return $annotations; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Annotation ::= "@" AnnotationName MethodCall Chris@0: * AnnotationName ::= QualifiedName | SimpleName Chris@0: * QualifiedName ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName Chris@0: * NameSpacePart ::= identifier | null | false | true Chris@0: * SimpleName ::= identifier | null | false | true Chris@0: * Chris@0: * @return mixed False if it is not a valid annotation. Chris@0: * Chris@0: * @throws AnnotationException Chris@0: */ Chris@0: private function Annotation() Chris@0: { Chris@0: $this->match(DocLexer::T_AT); Chris@0: Chris@0: // check if we have an annotation Chris@0: $name = $this->Identifier(); Chris@0: Chris@0: // only process names which are not fully qualified, yet Chris@0: // fully qualified names must start with a \ Chris@0: $originalName = $name; Chris@0: Chris@0: if ('\\' !== $name[0]) { Chris@12: $pos = strpos($name, '\\'); Chris@12: $alias = (false === $pos)? $name : substr($name, 0, $pos); Chris@0: $found = false; Chris@12: $loweredAlias = strtolower($alias); Chris@0: Chris@0: if ($this->namespaces) { Chris@0: foreach ($this->namespaces as $namespace) { Chris@0: if ($this->classExists($namespace.'\\'.$name)) { Chris@0: $name = $namespace.'\\'.$name; Chris@0: $found = true; Chris@0: break; Chris@0: } Chris@0: } Chris@12: } elseif (isset($this->imports[$loweredAlias])) { Chris@0: $found = true; Chris@0: $name = (false !== $pos) Chris@0: ? $this->imports[$loweredAlias] . substr($name, $pos) Chris@0: : $this->imports[$loweredAlias]; Chris@0: } elseif ( ! isset($this->ignoredAnnotationNames[$name]) Chris@0: && isset($this->imports['__NAMESPACE__']) Chris@0: && $this->classExists($this->imports['__NAMESPACE__'] . '\\' . $name) Chris@0: ) { Chris@0: $name = $this->imports['__NAMESPACE__'].'\\'.$name; Chris@0: $found = true; Chris@0: } elseif (! isset($this->ignoredAnnotationNames[$name]) && $this->classExists($name)) { Chris@0: $found = true; Chris@0: } Chris@0: Chris@0: if ( ! $found) { Chris@12: if ($this->isIgnoredAnnotation($name)) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: 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: } Chris@0: } Chris@0: Chris@12: $name = ltrim($name,'\\'); Chris@12: Chris@0: if ( ! $this->classExists($name)) { Chris@0: throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context)); Chris@0: } Chris@0: Chris@0: // at this point, $name contains the fully qualified class name of the Chris@0: // annotation, and it is also guaranteed that this class exists, and Chris@0: // that it is loaded Chris@0: Chris@0: Chris@0: // collects the metadata annotation only if there is not yet Chris@0: if ( ! isset(self::$annotationMetadata[$name])) { Chris@0: $this->collectAnnotationMetadata($name); Chris@0: } Chris@0: Chris@0: // verify that the class is really meant to be an annotation and not just any ordinary class Chris@0: if (self::$annotationMetadata[$name]['is_annotation'] === false) { Chris@12: if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$originalName])) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: 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: } Chris@0: Chris@0: //if target is nested annotation Chris@0: $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target; Chris@0: Chris@0: // Next will be nested Chris@0: $this->isNestedAnnotation = true; Chris@0: Chris@0: //if annotation does not support current target Chris@0: if (0 === (self::$annotationMetadata[$name]['targets'] & $target) && $target) { Chris@0: throw AnnotationException::semanticalError( Chris@0: sprintf('Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.', Chris@0: $originalName, $this->context, self::$annotationMetadata[$name]['targets_literal']) Chris@0: ); Chris@0: } Chris@0: Chris@0: $values = $this->MethodCall(); Chris@0: Chris@0: if (isset(self::$annotationMetadata[$name]['enum'])) { Chris@0: // checks all declared attributes Chris@0: foreach (self::$annotationMetadata[$name]['enum'] as $property => $enum) { Chris@0: // checks if the attribute is a valid enumerator Chris@0: if (isset($values[$property]) && ! in_array($values[$property], $enum['value'])) { Chris@0: throw AnnotationException::enumeratorError($property, $name, $this->context, $enum['literal'], $values[$property]); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // checks all declared attributes Chris@0: foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) { Chris@0: if ($property === self::$annotationMetadata[$name]['default_property'] Chris@0: && !isset($values[$property]) && isset($values['value'])) { Chris@0: $property = 'value'; Chris@0: } Chris@0: Chris@0: // handle a not given attribute or null value Chris@0: if (!isset($values[$property])) { Chris@0: if ($type['required']) { Chris@0: throw AnnotationException::requiredError($property, $originalName, $this->context, 'a(n) '.$type['value']); Chris@0: } Chris@0: Chris@0: continue; Chris@0: } Chris@0: Chris@0: if ($type['type'] === 'array') { Chris@0: // handle the case of a single value Chris@0: if ( ! is_array($values[$property])) { Chris@0: $values[$property] = array($values[$property]); Chris@0: } Chris@0: Chris@0: // checks if the attribute has array type declaration, such as "array" Chris@0: if (isset($type['array_type'])) { Chris@0: foreach ($values[$property] as $item) { Chris@0: if (gettype($item) !== $type['array_type'] && !$item instanceof $type['array_type']) { Chris@0: throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'either a(n) '.$type['array_type'].', or an array of '.$type['array_type'].'s', $item); Chris@0: } Chris@0: } Chris@0: } Chris@0: } elseif (gettype($values[$property]) !== $type['type'] && !$values[$property] instanceof $type['type']) { Chris@0: throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'a(n) '.$type['value'], $values[$property]); Chris@0: } Chris@0: } Chris@0: Chris@0: // check if the annotation expects values via the constructor, Chris@0: // or directly injected into public properties Chris@0: if (self::$annotationMetadata[$name]['has_constructor'] === true) { Chris@0: return new $name($values); Chris@0: } Chris@0: Chris@0: $instance = new $name(); Chris@0: Chris@0: foreach ($values as $property => $value) { Chris@0: if (!isset(self::$annotationMetadata[$name]['properties'][$property])) { Chris@0: if ('value' !== $property) { Chris@0: 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: } Chris@0: Chris@0: // handle the case if the property has no annotations Chris@0: if ( ! $property = self::$annotationMetadata[$name]['default_property']) { Chris@0: 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: } Chris@0: } Chris@0: Chris@0: $instance->{$property} = $value; Chris@0: } Chris@0: Chris@0: return $instance; Chris@0: } Chris@0: Chris@0: /** Chris@0: * MethodCall ::= ["(" [Values] ")"] Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function MethodCall() Chris@0: { Chris@0: $values = array(); Chris@0: Chris@0: if ( ! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) { Chris@0: return $values; Chris@0: } Chris@0: Chris@0: $this->match(DocLexer::T_OPEN_PARENTHESIS); Chris@0: Chris@0: if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) { Chris@0: $values = $this->Values(); Chris@0: } Chris@0: Chris@0: $this->match(DocLexer::T_CLOSE_PARENTHESIS); Chris@0: Chris@0: return $values; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Values ::= Array | Value {"," Value}* [","] Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function Values() Chris@0: { Chris@0: $values = array($this->Value()); Chris@0: Chris@0: while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { Chris@0: $this->match(DocLexer::T_COMMA); Chris@0: Chris@0: if ($this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) { Chris@0: break; Chris@0: } Chris@0: Chris@0: $token = $this->lexer->lookahead; Chris@0: $value = $this->Value(); Chris@0: Chris@0: if ( ! is_object($value) && ! is_array($value)) { Chris@0: $this->syntaxError('Value', $token); Chris@0: } Chris@0: Chris@0: $values[] = $value; Chris@0: } Chris@0: Chris@0: foreach ($values as $k => $value) { Chris@0: if (is_object($value) && $value instanceof \stdClass) { Chris@0: $values[$value->name] = $value->value; Chris@0: } else if ( ! isset($values['value'])){ Chris@0: $values['value'] = $value; Chris@0: } else { Chris@0: if ( ! is_array($values['value'])) { Chris@0: $values['value'] = array($values['value']); Chris@0: } Chris@0: Chris@0: $values['value'][] = $value; Chris@0: } Chris@0: Chris@0: unset($values[$k]); Chris@0: } Chris@0: Chris@0: return $values; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Constant ::= integer | string | float | boolean Chris@0: * Chris@0: * @return mixed Chris@0: * Chris@0: * @throws AnnotationException Chris@0: */ Chris@0: private function Constant() Chris@0: { Chris@0: $identifier = $this->Identifier(); Chris@0: Chris@0: if ( ! defined($identifier) && false !== strpos($identifier, '::') && '\\' !== $identifier[0]) { Chris@0: list($className, $const) = explode('::', $identifier); Chris@0: Chris@12: $pos = strpos($className, '\\'); Chris@12: $alias = (false === $pos) ? $className : substr($className, 0, $pos); Chris@0: $found = false; Chris@12: $loweredAlias = strtolower($alias); Chris@0: Chris@0: switch (true) { Chris@0: case !empty ($this->namespaces): Chris@0: foreach ($this->namespaces as $ns) { Chris@0: if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) { Chris@0: $className = $ns.'\\'.$className; Chris@0: $found = true; Chris@0: break; Chris@0: } Chris@0: } Chris@0: break; Chris@0: Chris@12: case isset($this->imports[$loweredAlias]): Chris@0: $found = true; Chris@0: $className = (false !== $pos) Chris@0: ? $this->imports[$loweredAlias] . substr($className, $pos) Chris@0: : $this->imports[$loweredAlias]; Chris@0: break; Chris@0: Chris@0: default: Chris@0: if(isset($this->imports['__NAMESPACE__'])) { Chris@0: $ns = $this->imports['__NAMESPACE__']; Chris@0: Chris@0: if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) { Chris@0: $className = $ns.'\\'.$className; Chris@0: $found = true; Chris@0: } Chris@0: } Chris@0: break; Chris@0: } Chris@0: Chris@0: if ($found) { Chris@0: $identifier = $className . '::' . $const; Chris@0: } Chris@0: } Chris@0: Chris@0: // checks if identifier ends with ::class, \strlen('::class') === 7 Chris@0: $classPos = stripos($identifier, '::class'); Chris@0: if ($classPos === strlen($identifier) - 7) { Chris@0: return substr($identifier, 0, $classPos); Chris@0: } Chris@0: Chris@0: if (!defined($identifier)) { Chris@0: throw AnnotationException::semanticalErrorConstants($identifier, $this->context); Chris@0: } Chris@0: Chris@0: return constant($identifier); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Identifier ::= string Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: private function Identifier() Chris@0: { Chris@0: // check if we have an annotation Chris@0: if ( ! $this->lexer->isNextTokenAny(self::$classIdentifiers)) { Chris@0: $this->syntaxError('namespace separator or identifier'); Chris@0: } Chris@0: Chris@0: $this->lexer->moveNext(); Chris@0: Chris@0: $className = $this->lexer->token['value']; Chris@0: Chris@0: while ($this->lexer->lookahead['position'] === ($this->lexer->token['position'] + strlen($this->lexer->token['value'])) Chris@0: && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) { Chris@0: Chris@0: $this->match(DocLexer::T_NAMESPACE_SEPARATOR); Chris@0: $this->matchAny(self::$classIdentifiers); Chris@0: Chris@0: $className .= '\\' . $this->lexer->token['value']; Chris@0: } Chris@0: Chris@0: return $className; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Value ::= PlainValue | FieldAssignment Chris@0: * Chris@0: * @return mixed Chris@0: */ Chris@0: private function Value() Chris@0: { Chris@0: $peek = $this->lexer->glimpse(); Chris@0: Chris@0: if (DocLexer::T_EQUALS === $peek['type']) { Chris@0: return $this->FieldAssignment(); Chris@0: } Chris@0: Chris@0: return $this->PlainValue(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * PlainValue ::= integer | string | float | boolean | Array | Annotation Chris@0: * Chris@0: * @return mixed Chris@0: */ Chris@0: private function PlainValue() Chris@0: { Chris@0: if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) { Chris@0: return $this->Arrayx(); Chris@0: } Chris@0: Chris@0: if ($this->lexer->isNextToken(DocLexer::T_AT)) { Chris@0: return $this->Annotation(); Chris@0: } Chris@0: Chris@0: if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) { Chris@0: return $this->Constant(); Chris@0: } Chris@0: Chris@0: switch ($this->lexer->lookahead['type']) { Chris@0: case DocLexer::T_STRING: Chris@0: $this->match(DocLexer::T_STRING); Chris@0: return $this->lexer->token['value']; Chris@0: Chris@0: case DocLexer::T_INTEGER: Chris@0: $this->match(DocLexer::T_INTEGER); Chris@0: return (int)$this->lexer->token['value']; Chris@0: Chris@0: case DocLexer::T_FLOAT: Chris@0: $this->match(DocLexer::T_FLOAT); Chris@0: return (float)$this->lexer->token['value']; Chris@0: Chris@0: case DocLexer::T_TRUE: Chris@0: $this->match(DocLexer::T_TRUE); Chris@0: return true; Chris@0: Chris@0: case DocLexer::T_FALSE: Chris@0: $this->match(DocLexer::T_FALSE); Chris@0: return false; Chris@0: Chris@0: case DocLexer::T_NULL: Chris@0: $this->match(DocLexer::T_NULL); Chris@0: return null; Chris@0: Chris@0: default: Chris@0: $this->syntaxError('PlainValue'); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * FieldAssignment ::= FieldName "=" PlainValue Chris@0: * FieldName ::= identifier Chris@0: * Chris@12: * @return \stdClass Chris@0: */ Chris@0: private function FieldAssignment() Chris@0: { Chris@0: $this->match(DocLexer::T_IDENTIFIER); Chris@0: $fieldName = $this->lexer->token['value']; Chris@0: Chris@0: $this->match(DocLexer::T_EQUALS); Chris@0: Chris@0: $item = new \stdClass(); Chris@0: $item->name = $fieldName; Chris@0: $item->value = $this->PlainValue(); Chris@0: Chris@0: return $item; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}" Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function Arrayx() Chris@0: { Chris@0: $array = $values = array(); Chris@0: Chris@0: $this->match(DocLexer::T_OPEN_CURLY_BRACES); Chris@0: Chris@0: // If the array is empty, stop parsing and return. Chris@0: if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) { Chris@0: $this->match(DocLexer::T_CLOSE_CURLY_BRACES); Chris@0: Chris@0: return $array; Chris@0: } Chris@0: Chris@0: $values[] = $this->ArrayEntry(); Chris@0: Chris@0: while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { Chris@0: $this->match(DocLexer::T_COMMA); Chris@0: Chris@0: // optional trailing comma Chris@0: if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) { Chris@0: break; Chris@0: } Chris@0: Chris@0: $values[] = $this->ArrayEntry(); Chris@0: } Chris@0: Chris@0: $this->match(DocLexer::T_CLOSE_CURLY_BRACES); Chris@0: Chris@0: foreach ($values as $value) { Chris@0: list ($key, $val) = $value; Chris@0: Chris@0: if ($key !== null) { Chris@0: $array[$key] = $val; Chris@0: } else { Chris@0: $array[] = $val; Chris@0: } Chris@0: } Chris@0: Chris@0: return $array; Chris@0: } Chris@0: Chris@0: /** Chris@0: * ArrayEntry ::= Value | KeyValuePair Chris@0: * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant Chris@0: * Key ::= string | integer | Constant Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function ArrayEntry() Chris@0: { Chris@0: $peek = $this->lexer->glimpse(); Chris@0: Chris@0: if (DocLexer::T_EQUALS === $peek['type'] Chris@0: || DocLexer::T_COLON === $peek['type']) { Chris@0: Chris@0: if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) { Chris@0: $key = $this->Constant(); Chris@0: } else { Chris@0: $this->matchAny(array(DocLexer::T_INTEGER, DocLexer::T_STRING)); Chris@0: $key = $this->lexer->token['value']; Chris@0: } Chris@0: Chris@0: $this->matchAny(array(DocLexer::T_EQUALS, DocLexer::T_COLON)); Chris@0: Chris@0: return array($key, $this->PlainValue()); Chris@0: } Chris@0: Chris@0: return array(null, $this->Value()); Chris@0: } Chris@12: Chris@12: /** Chris@12: * Checks whether the given $name matches any ignored annotation name or namespace Chris@12: * Chris@12: * @param string $name Chris@12: * Chris@12: * @return bool Chris@12: */ Chris@12: private function isIgnoredAnnotation($name) Chris@12: { Chris@12: if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) { Chris@12: return true; Chris@12: } Chris@12: Chris@12: foreach (array_keys($this->ignoredAnnotationNamespaces) as $ignoredAnnotationNamespace) { Chris@12: $ignoredAnnotationNamespace = rtrim($ignoredAnnotationNamespace, '\\') . '\\'; Chris@12: Chris@12: if (0 === stripos(rtrim($name, '\\') . '\\', $ignoredAnnotationNamespace)) { Chris@12: return true; Chris@12: } Chris@12: } Chris@12: Chris@12: return false; Chris@12: } Chris@0: }