. */ namespace Doctrine\Common\Annotations; use Closure; use ReflectionClass; /** * A parser for docblock annotations. * * It is strongly discouraged to change the default annotation parsing process. * * @author Benjamin Eberlei * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel * @author Johannes M. Schmitt */ final class DocParser { /** * An array of all valid tokens for a class name. * * @var array */ private static $classIdentifiers = array(DocLexer::T_IDENTIFIER, DocLexer::T_TRUE, DocLexer::T_FALSE, DocLexer::T_NULL); /** * The lexer. * * @var Doctrine\Common\Annotations\DocLexer */ private $lexer; /** * Flag to control if the current annotation is nested or not. * * @var boolean */ private $isNestedAnnotation = false; /** * Hashmap containing all use-statements that are to be used when parsing * the given doc block. * * @var array */ private $imports = array(); /** * This hashmap is used internally to cache results of class_exists() * look-ups. * * @var array */ private $classExists = array(); /** * * @var This hashmap is used internally to cache if a class is an annotation or not. * * @var array */ private $isAnnotation = array(); /** * Whether annotations that have not been imported should be ignored. * * @var boolean */ private $ignoreNotImportedAnnotations = false; /** * A list with annotations that are not causing exceptions when not resolved to an annotation class. * * The names must be the raw names as used in the class, not the fully qualified * class names. * * @var array */ private $ignoredAnnotationNames = array(); /** * @var array */ private $namespaceAliases = array(); /** * @var string */ private $context = ''; /** * @var Closure */ private $creationFn = null; /** * Constructs a new DocParser. */ public function __construct() { $this->lexer = new DocLexer; } /** * Sets the annotation names that are ignored during the parsing process. * * The names are supposed to be the raw names as used in the class, not the * fully qualified class names. * * @param array $names */ public function setIgnoredAnnotationNames(array $names) { $this->ignoredAnnotationNames = $names; } /** * @deprecated Will be removed in 3.0 * @param \Closure $func */ public function setAnnotationCreationFunction(\Closure $func) { $this->creationFn = $func; } public function setImports(array $imports) { $this->imports = $imports; } public function setIgnoreNotImportedAnnotations($bool) { $this->ignoreNotImportedAnnotations = (Boolean) $bool; } public function setAnnotationNamespaceAlias($namespace, $alias) { $this->namespaceAliases[$alias] = $namespace; } /** * Parses the given docblock string for annotations. * * @param string $input The docblock string to parse. * @param string $context The parsing context. * @return array Array of annotations. If no annotations are found, an empty array is returned. */ public function parse($input, $context = '') { if (false === $pos = strpos($input, '@')) { return array(); } // also parse whatever character is before the @ if ($pos > 0) { $pos -= 1; } $this->context = $context; $this->lexer->setInput(trim(substr($input, $pos), '* /')); $this->lexer->moveNext(); return $this->Annotations(); } /** * Attempts to match the given token with the current lookahead token. * If they match, updates the lookahead token; otherwise raises a syntax error. * * @param int Token type. * @return bool True if tokens match; false otherwise. */ private function match($token) { if ( ! $this->lexer->isNextToken($token) ) { $this->syntaxError($this->lexer->getLiteral($token)); } return $this->lexer->moveNext(); } /** * Attempts to match the current lookahead token with any of the given tokens. * * If any of them matches, this method updates the lookahead token; otherwise * a syntax error is raised. * * @param array $tokens * @return bool */ private function matchAny(array $tokens) { if ( ! $this->lexer->isNextTokenAny($tokens)) { $this->syntaxError(implode(' or ', array_map(array($this->lexer, 'getLiteral'), $tokens))); } return $this->lexer->moveNext(); } /** * Generates a new syntax error. * * @param string $expected Expected string. * @param array $token Optional token. * @throws SyntaxException */ private function syntaxError($expected, $token = null) { if ($token === null) { $token = $this->lexer->lookahead; } $message = "Expected {$expected}, got "; if ($this->lexer->lookahead === null) { $message .= 'end of string'; } else { $message .= "'{$token['value']}' at position {$token['position']}"; } if (strlen($this->context)) { $message .= ' in ' . $this->context; } $message .= '.'; throw AnnotationException::syntaxError($message); } /** * Attempt to check if a class exists or not. This never goes through the PHP autoloading mechanism * but uses the {@link AnnotationRegistry} to load classes. * * @param string $fqcn * @return boolean */ private function classExists($fqcn) { if (isset($this->classExists[$fqcn])) { return $this->classExists[$fqcn]; } // first check if the class already exists, maybe loaded through another AnnotationReader if (class_exists($fqcn, false)) { return $this->classExists[$fqcn] = true; } // final check, does this class exist? return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn); } /** * Annotations ::= Annotation {[ "*" ]* [Annotation]}* * * @return array */ private function Annotations() { $annotations = array(); while (null !== $this->lexer->lookahead) { if (DocLexer::T_AT !== $this->lexer->lookahead['type']) { $this->lexer->moveNext(); continue; } // make sure the @ is preceded by non-catchable pattern if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) { $this->lexer->moveNext(); continue; } // make sure the @ is followed by either a namespace separator, or // an identifier token if ((null === $peek = $this->lexer->glimpse()) || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true)) || $peek['position'] !== $this->lexer->lookahead['position'] + 1) { $this->lexer->moveNext(); continue; } $this->isNestedAnnotation = false; if (false !== $annot = $this->Annotation()) { $annotations[] = $annot; } } return $annotations; } /** * Annotation ::= "@" AnnotationName ["(" [Values] ")"] * AnnotationName ::= QualifiedName | SimpleName * QualifiedName ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName * NameSpacePart ::= identifier | null | false | true * SimpleName ::= identifier | null | false | true * * @return mixed False if it is not a valid annotation. */ private function Annotation() { $this->match(DocLexer::T_AT); // check if we have an annotation if ($this->lexer->isNextTokenAny(self::$classIdentifiers)) { $this->lexer->moveNext(); $name = $this->lexer->token['value']; } else if ($this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) { $name = ''; } else { $this->syntaxError('namespace separator or identifier'); } while ($this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value']) && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) { $this->match(DocLexer::T_NAMESPACE_SEPARATOR); $this->matchAny(self::$classIdentifiers); $name .= '\\'.$this->lexer->token['value']; } if (strpos($name, ":") !== false) { list ($alias, $name) = explode(':', $name); // If the namespace alias doesnt exist, skip until next annotation if ( ! isset($this->namespaceAliases[$alias])) { $this->lexer->skipUntil(DocLexer::T_AT); return false; } $name = $this->namespaceAliases[$alias] . $name; } // only process names which are not fully qualified, yet if ('\\' !== $name[0] && !$this->classExists($name)) { $alias = (false === $pos = strpos($name, '\\'))? $name : substr($name, 0, $pos); if (isset($this->imports[$loweredAlias = strtolower($alias)])) { if (false !== $pos) { $name = $this->imports[$loweredAlias].substr($name, $pos); } else { $name = $this->imports[$loweredAlias]; } } elseif (isset($this->imports['__DEFAULT__']) && $this->classExists($this->imports['__DEFAULT__'].$name)) { $name = $this->imports['__DEFAULT__'].$name; } elseif (isset($this->imports['__NAMESPACE__']) && $this->classExists($this->imports['__NAMESPACE__'].'\\'.$name)) { $name = $this->imports['__NAMESPACE__'].'\\'.$name; } else { if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) { return false; } throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s was never imported.', $name, $this->context)); } } if (!$this->classExists($name)) { throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context)); } if (!$this->isAnnotation($name)) { return false; } // Verifies that the annotation class extends any class that contains "Annotation". // This is done to avoid coupling of Doctrine Annotations against other libraries. // at this point, $name contains the fully qualified class name of the // annotation, and it is also guaranteed that this class exists, and // that it is loaded // Next will be nested $this->isNestedAnnotation = true; $values = array(); if ($this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) { $this->match(DocLexer::T_OPEN_PARENTHESIS); if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) { $values = $this->Values(); } $this->match(DocLexer::T_CLOSE_PARENTHESIS); } return $this->newAnnotation($name, $values); } /** * Verify that the found class is actually an annotation. * * This can be detected through two mechanisms: * 1. Class extends Doctrine\Common\Annotations\Annotation * 2. The class level docblock contains the string "@Annotation" * * @param string $name * @return bool */ private function isAnnotation($name) { if (!isset($this->isAnnotation[$name])) { if (is_subclass_of($name, 'Doctrine\Common\Annotations\Annotation')) { $this->isAnnotation[$name] = true; } else { $reflClass = new \ReflectionClass($name); $this->isAnnotation[$name] = strpos($reflClass->getDocComment(), "@Annotation") !== false; } } return $this->isAnnotation[$name]; } private function newAnnotation($name, $values) { if ($this->creationFn !== null) { $fn = $this->creationFn; return $fn($name, $values); } return new $name($values); } /** * Values ::= Array | Value {"," Value}* * * @return array */ private function Values() { $values = array(); // Handle the case of a single array as value, i.e. @Foo({....}) if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) { $values['value'] = $this->Value(); return $values; } $values[] = $this->Value(); while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { $this->match(DocLexer::T_COMMA); $token = $this->lexer->lookahead; $value = $this->Value(); if ( ! is_object($value) && ! is_array($value)) { $this->syntaxError('Value', $token); } $values[] = $value; } foreach ($values as $k => $value) { if (is_object($value) && $value instanceof \stdClass) { $values[$value->name] = $value->value; } else if ( ! isset($values['value'])){ $values['value'] = $value; } else { if ( ! is_array($values['value'])) { $values['value'] = array($values['value']); } $values['value'][] = $value; } unset($values[$k]); } return $values; } /** * Value ::= PlainValue | FieldAssignment * * @return mixed */ private function Value() { $peek = $this->lexer->glimpse(); if (DocLexer::T_EQUALS === $peek['type']) { return $this->FieldAssignment(); } return $this->PlainValue(); } /** * PlainValue ::= integer | string | float | boolean | Array | Annotation * * @return mixed */ private function PlainValue() { if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) { return $this->Arrayx(); } if ($this->lexer->isNextToken(DocLexer::T_AT)) { return $this->Annotation(); } switch ($this->lexer->lookahead['type']) { case DocLexer::T_STRING: $this->match(DocLexer::T_STRING); return $this->lexer->token['value']; case DocLexer::T_INTEGER: $this->match(DocLexer::T_INTEGER); return (int)$this->lexer->token['value']; case DocLexer::T_FLOAT: $this->match(DocLexer::T_FLOAT); return (float)$this->lexer->token['value']; case DocLexer::T_TRUE: $this->match(DocLexer::T_TRUE); return true; case DocLexer::T_FALSE: $this->match(DocLexer::T_FALSE); return false; case DocLexer::T_NULL: $this->match(DocLexer::T_NULL); return null; default: $this->syntaxError('PlainValue'); } } /** * FieldAssignment ::= FieldName "=" PlainValue * FieldName ::= identifier * * @return array */ private function FieldAssignment() { $this->match(DocLexer::T_IDENTIFIER); $fieldName = $this->lexer->token['value']; $this->match(DocLexer::T_EQUALS); $item = new \stdClass(); $item->name = $fieldName; $item->value = $this->PlainValue(); return $item; } /** * Array ::= "{" ArrayEntry {"," ArrayEntry}* "}" * * @return array */ private function Arrayx() { $array = $values = array(); $this->match(DocLexer::T_OPEN_CURLY_BRACES); $values[] = $this->ArrayEntry(); while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { $this->match(DocLexer::T_COMMA); $values[] = $this->ArrayEntry(); } $this->match(DocLexer::T_CLOSE_CURLY_BRACES); foreach ($values as $value) { list ($key, $val) = $value; if ($key !== null) { $array[$key] = $val; } else { $array[] = $val; } } return $array; } /** * ArrayEntry ::= Value | KeyValuePair * KeyValuePair ::= Key "=" PlainValue * Key ::= string | integer * * @return array */ private function ArrayEntry() { $peek = $this->lexer->glimpse(); if (DocLexer::T_EQUALS === $peek['type']) { $this->match( $this->lexer->isNextToken(DocLexer::T_INTEGER) ? DocLexer::T_INTEGER : DocLexer::T_STRING ); $key = $this->lexer->token['value']; $this->match(DocLexer::T_EQUALS); return array($key, $this->PlainValue()); } return array(null, $this->Value()); } }