<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\Common\Annotations;
use Closure, Doctrine\Common\ClassLoader;
/**
* A simple parser for docblock annotations.
*
* This Parser can be subclassed to customize certain aspects of the annotation
* parsing and/or creation process. Note though that currently no special care
* is taken to maintain full backwards compatibility for subclasses. Implementation
* details of the default Parser can change without explicit notice.
*
* @since 2.0
* @author Benjamin Eberlei <hide@address.com>
* @author Guilherme Blanco <hide@address.com>
* @author Jonathan Wage <hide@address.com>
* @author Roman Borschel <hide@address.com>
*/
class Parser
{
/**
* Some common tags that are stripped prior to parsing in order to reduce parsing overhead.
*
* @var array
*/
private static $strippedTags = array(
"{@internal", "{@inheritdoc", "{@link"
);
/**
* The lexer.
*
* @var Doctrine\Common\Annotations\Lexer
*/
private $lexer;
/**
* Flag to control if the current annotation is nested or not.
*
* @var boolean
*/
protected $isNestedAnnotation = false;
/**
* Default namespace for annotations.
*
* @var string
*/
private $defaultAnnotationNamespace = '';
/**
* Hashmap to store namespace aliases.
*
* @var array
*/
private $namespaceAliases = array();
/**
* @var string
*/
private $context = '';
/**
* @var boolean Whether to try to autoload annotations that are not yet defined.
*/
private $autoloadAnnotations = false;
/**
* @var Closure The custom function used to create new annotations, if any.
*/
private $annotationCreationFunction;
/**
* Constructs a new AnnotationParser.
*/
public function __construct(Lexer $lexer = null)
{
$this->lexer = $lexer ?: new Lexer;
}
/**
* Gets the lexer used by this parser.
*
* @return Lexer The lexer.
*/
public function getLexer()
{
return $this->lexer;
}
/**
* Sets a flag whether to try to autoload annotation classes, as well as to distinguish
* between what is an annotation and what not by triggering autoloading.
*
* NOTE: Autoloading of annotation classes is inefficient and requires silently failing
* autoloaders. In particular, setting this option to TRUE renders the Parser
* incompatible with a {@link ClassLoader}.
* @param boolean $bool Boolean flag.
*/
public function setAutoloadAnnotations($bool)
{
$this->autoloadAnnotations = $bool;
}
/**
* Sets the custom function to use for creating new annotations.
*
* The function is supplied two arguments. The first argument is the name
* of the annotation and the second argument an array of values for this
* annotation. The function is assumed to return an object or NULL.
* Whenever the function returns NULL for an annotation, the parser falls
* back to the default annotation creation process.
*
* Whenever the function returns NULL for an annotation, the implementation falls
* back to the default annotation creation process.
*
* @param Closure $func
*/
public function setAnnotationCreationFunction(Closure $func)
{
$this->annotationCreationFunction = $func;
}
/**
* Gets a flag whether to try to autoload annotation classes.
*
* @see setAutoloadAnnotations
* @return boolean
*/
public function getAutoloadAnnotations()
{
return $this->autoloadAnnotations;
}
/**
* Sets the default namespace that is assumed for an annotation that does not
* define a namespace prefix.
*
* @param string $defaultNamespace
*/
public function setDefaultAnnotationNamespace($defaultNamespace)
{
$this->defaultAnnotationNamespace = $defaultNamespace;
}
/**
* Sets an alias for an annotation namespace.
*
* @param string $namespace
* @param string $alias
*/
public function setAnnotationNamespaceAlias($namespace, $alias)
{
$this->namespaceAliases[$alias] = $namespace;
}
/**
* Gets the namespace alias mappings used by this parser.
*
* @return array The namespace alias mappings.
*/
public function getNamespaceAliases()
{
return $this->namespaceAliases;
}
/**
* Parses the given docblock string for annotations.
*
* @param string $docBlockString 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($docBlockString, $context='')
{
// Strip out some known inline tags.
$input = str_replace(self::$strippedTags, '', $docBlockString);
if (false === $pos = strpos($input, '@')) {
return array();
}
// also parse whatever character is before the @
if ($pos > 0) {
$pos -= 1;
}
$this->context = $context;
$this->lexer->reset();
$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.
*/
public function match($token)
{
if ( ! ($this->lexer->lookahead['type'] === $token)) {
$this->syntaxError($this->lexer->getLiteral($token));
}
$this->lexer->moveNext();
}
/**
* Generates a new syntax error.
*
* @param string $expected Expected string.
* @param array $token Optional token.
* @throws AnnotationException
*/
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);
}
/**
* Annotations ::= Annotation {[ "*" ]* [Annotation]}*
*
* @return array
*/
public function Annotations()
{
$annotations = array();
while (null !== $this->lexer->lookahead) {
if (Lexer::T_AT !== $this->lexer->lookahead['type']) {
$this->lexer->moveNext();
continue;
}
// make sure the @ is preceeded 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 (($peek = $this->lexer->glimpse()) === null ||
(Lexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && Lexer::T_IDENTIFIER !== $peek['type']) ||
$peek['position'] !== $this->lexer->lookahead['position'] + 1) {
$this->lexer->moveNext();
continue;
}
$this->isNestedAnnotation = false;
if (($annot = $this->Annotation()) !== false) {
$class = get_class($annot);
if (isset($annotations[$class])) {
if ( ! is_array($annotations[$class])) {
$annotations[$class] = array($annotations[$class]);
}
$annotations[$class][] = $annot;
} else {
$annotations[$class] = $annot;
}
}
}
return $annotations;
}
/**
* Annotation ::= "@" AnnotationName ["(" [Values] ")"]
* AnnotationName ::= QualifiedName | SimpleName | AliasedName
* QualifiedName ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName
* AliasedName ::= Alias ":" SimpleName
* NameSpacePart ::= identifier
* SimpleName ::= identifier
* Alias ::= identifier
*
* @return mixed False if it is not a valid annotation.
*/
public function Annotation()
{
$values = array();
$nameParts = array();
$this->match(Lexer::T_AT);
if ($this->isNestedAnnotation === false) {
if ($this->lexer->lookahead['type'] !== Lexer::T_IDENTIFIER) {
return false;
}
$this->lexer->moveNext();
} else {
$this->match(Lexer::T_IDENTIFIER);
}
$nameParts[] = $this->lexer->token['value'];
while ($this->lexer->isNextToken(Lexer::T_NAMESPACE_SEPARATOR)) {
$this->match(Lexer::T_NAMESPACE_SEPARATOR);
$this->match(Lexer::T_IDENTIFIER);
$nameParts[] = $this->lexer->token['value'];
}
// Effectively pick the name of the class (append default NS if none, grab from NS alias, etc)
$namespacedAnnotation = false;
if (strpos($nameParts[0], ':')) {
list ($alias, $nameParts[0]) = explode(':', $nameParts[0]);
// If the namespace alias doesnt exist, skip until next annotation
if ( ! isset($this->namespaceAliases[$alias])) {
$this->lexer->skipUntil(Lexer::T_AT);
return false;
}
$name = $this->namespaceAliases[$alias] . implode('\\', $nameParts);
$namespacedAnnotation = true;
} else if (count($nameParts) == 1) {
$name = $this->defaultAnnotationNamespace . $nameParts[0];
} else {
$name = implode('\\', $nameParts);
}
// Does the annotation class exist?
if ( ! class_exists($name, $this->autoloadAnnotations)) {
$this->lexer->skipUntil(Lexer::T_AT);
return false;
}
// Next will be nested
$this->isNestedAnnotation = true;
if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
$this->match(Lexer::T_OPEN_PARENTHESIS);
if ( ! $this->lexer->isNextToken(Lexer::T_CLOSE_PARENTHESIS)) {
$values = $this->Values();
}
$this->match(Lexer::T_CLOSE_PARENTHESIS);
}
if ($this->annotationCreationFunction !== null) {
$func = $this->annotationCreationFunction;
$annot = $func($name, $values);
}
return isset($annot) ? $annot : $this->newAnnotation($name, $values);
}
/**
* Values ::= Array | Value {"," Value}*
*
* @return array
*/
public function Values()
{
$values = array();
// Handle the case of a single array as value, i.e. @Foo({....})
if ($this->lexer->isNextToken(Lexer::T_OPEN_CURLY_BRACES)) {
$values['value'] = $this->Value();
return $values;
}
$values[] = $this->Value();
while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
$this->match(Lexer::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
*/
public function Value()
{
$peek = $this->lexer->glimpse();
if ($peek['value'] === '=') {
return $this->FieldAssignment();
}
return $this->PlainValue();
}
/**
* PlainValue ::= integer | string | float | boolean | Array | Annotation
*
* @return mixed
*/
public function PlainValue()
{
if ($this->lexer->isNextToken(Lexer::T_OPEN_CURLY_BRACES)) {
return $this->Arrayx();
}
if ($this->lexer->isNextToken(Lexer::T_AT)) {
return $this->Annotation();
}
switch ($this->lexer->lookahead['type']) {
case Lexer::T_STRING:
$this->match(Lexer::T_STRING);
return $this->lexer->token['value'];
case Lexer::T_INTEGER:
$this->match(Lexer::T_INTEGER);
return (int)$this->lexer->token['value'];
case Lexer::T_FLOAT:
$this->match(Lexer::T_FLOAT);
return (float)$this->lexer->token['value'];
case Lexer::T_TRUE:
$this->match(Lexer::T_TRUE);
return true;
case Lexer::T_FALSE:
$this->match(Lexer::T_FALSE);
return false;
case Lexer::T_NULL:
$this->match(Lexer::T_NULL);
return null;
default:
$this->syntaxError('PlainValue');
}
}
/**
* FieldAssignment ::= FieldName "=" PlainValue
* FieldName ::= identifier
*
* @return array
*/
public function FieldAssignment()
{
$this->match(Lexer::T_IDENTIFIER);
$fieldName = $this->lexer->token['value'];
$this->match(Lexer::T_EQUALS);
$item = new \stdClass();
$item->name = $fieldName;
$item->value = $this->PlainValue();
return $item;
}
/**
* Array ::= "{" ArrayEntry {"," ArrayEntry}* "}"
*
* @return array
*/
public function Arrayx()
{
$array = $values = array();
$this->match(Lexer::T_OPEN_CURLY_BRACES);
$values[] = $this->ArrayEntry();
while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
$this->match(Lexer::T_COMMA);
$values[] = $this->ArrayEntry();
}
$this->match(Lexer::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
*/
public function ArrayEntry()
{
$peek = $this->lexer->glimpse();
if ($peek['value'] == '=') {
$this->match(
$this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_STRING
);
$key = $this->lexer->token['value'];
$this->match(Lexer::T_EQUALS);
return array($key, $this->PlainValue());
}
return array(null, $this->Value());
}
/**
* Constructs a new annotation with a given map of values.
*
* The default construction procedure is to instantiate a new object of a class
* with the same name as the annotation. Subclasses can override this method to
* change the construction process of new annotations.
*
* @param string The name of the annotation.
* @param array The map of annotation values.
* @return mixed The new annotation with the given values.
*/
protected function newAnnotation($name, array $values)
{
return new $name($values);
}
}