vendor/doctrine-common/lib/Doctrine/Common/Annotations/DocParser.php
changeset 0 7f95f8617b0b
equal deleted inserted replaced
-1:000000000000 0:7f95f8617b0b
       
     1 <?php
       
     2 /*
       
     3  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
       
     4  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
       
     5  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
       
     6  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
       
     7  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
       
     8  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
       
     9  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
       
    10  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
       
    11  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
       
    12  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
       
    13  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       
    14  *
       
    15  * This software consists of voluntary contributions made by many individuals
       
    16  * and is licensed under the LGPL. For more information, see
       
    17  * <http://www.doctrine-project.org>.
       
    18  */
       
    19 
       
    20 namespace Doctrine\Common\Annotations;
       
    21 
       
    22 use Closure;
       
    23 use ReflectionClass;
       
    24 
       
    25 /**
       
    26  * A parser for docblock annotations.
       
    27  *
       
    28  * It is strongly discouraged to change the default annotation parsing process.
       
    29  *
       
    30  * @author Benjamin Eberlei <kontakt@beberlei.de>
       
    31  * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
       
    32  * @author Jonathan Wage <jonwage@gmail.com>
       
    33  * @author Roman Borschel <roman@code-factory.org>
       
    34  * @author Johannes M. Schmitt <schmittjoh@gmail.com>
       
    35  */
       
    36 final class DocParser
       
    37 {
       
    38     /**
       
    39      * An array of all valid tokens for a class name.
       
    40      *
       
    41      * @var array
       
    42      */
       
    43     private static $classIdentifiers = array(DocLexer::T_IDENTIFIER, DocLexer::T_TRUE, DocLexer::T_FALSE, DocLexer::T_NULL);
       
    44 
       
    45     /**
       
    46      * The lexer.
       
    47      *
       
    48      * @var Doctrine\Common\Annotations\DocLexer
       
    49      */
       
    50     private $lexer;
       
    51 
       
    52     /**
       
    53      * Flag to control if the current annotation is nested or not.
       
    54      *
       
    55      * @var boolean
       
    56      */
       
    57     private $isNestedAnnotation = false;
       
    58 
       
    59     /**
       
    60      * Hashmap containing all use-statements that are to be used when parsing
       
    61      * the given doc block.
       
    62      *
       
    63      * @var array
       
    64      */
       
    65     private $imports = array();
       
    66 
       
    67     /**
       
    68      * This hashmap is used internally to cache results of class_exists()
       
    69      * look-ups.
       
    70      *
       
    71      * @var array
       
    72      */
       
    73     private $classExists = array();
       
    74     
       
    75     /**
       
    76      *
       
    77      * @var This hashmap is used internally to cache if a class is an annotation or not.
       
    78      * 
       
    79      * @var array
       
    80      */
       
    81     private $isAnnotation = array();
       
    82 
       
    83     /**
       
    84      * Whether annotations that have not been imported should be ignored.
       
    85      *
       
    86      * @var boolean
       
    87      */
       
    88     private $ignoreNotImportedAnnotations = false;
       
    89 
       
    90     /**
       
    91      * A list with annotations that are not causing exceptions when not resolved to an annotation class.
       
    92      *
       
    93      * The names must be the raw names as used in the class, not the fully qualified
       
    94      * class names.
       
    95      *
       
    96      * @var array
       
    97      */
       
    98     private $ignoredAnnotationNames = array();
       
    99 
       
   100     /**
       
   101      * @var array
       
   102      */
       
   103     private $namespaceAliases = array();
       
   104 
       
   105     /**
       
   106      * @var string
       
   107      */
       
   108     private $context = '';
       
   109     
       
   110     /**
       
   111      * @var Closure
       
   112      */
       
   113     private $creationFn = null;
       
   114 
       
   115     /**
       
   116      * Constructs a new DocParser.
       
   117      */
       
   118     public function __construct()
       
   119     {
       
   120         $this->lexer = new DocLexer;
       
   121     }
       
   122 
       
   123     /**
       
   124      * Sets the annotation names that are ignored during the parsing process.
       
   125      *
       
   126      * The names are supposed to be the raw names as used in the class, not the
       
   127      * fully qualified class names.
       
   128      *
       
   129      * @param array $names
       
   130      */
       
   131     public function setIgnoredAnnotationNames(array $names)
       
   132     {
       
   133         $this->ignoredAnnotationNames = $names;
       
   134     }
       
   135     
       
   136     /**
       
   137      * @deprecated Will be removed in 3.0
       
   138      * @param \Closure $func
       
   139      */
       
   140     public function setAnnotationCreationFunction(\Closure $func)
       
   141     {
       
   142         $this->creationFn = $func;
       
   143     }
       
   144 
       
   145     public function setImports(array $imports)
       
   146     {
       
   147         $this->imports = $imports;
       
   148     }
       
   149 
       
   150     public function setIgnoreNotImportedAnnotations($bool)
       
   151     {
       
   152         $this->ignoreNotImportedAnnotations = (Boolean) $bool;
       
   153     }
       
   154 
       
   155     public function setAnnotationNamespaceAlias($namespace, $alias)
       
   156     {
       
   157         $this->namespaceAliases[$alias] = $namespace;
       
   158     }
       
   159 
       
   160     /**
       
   161      * Parses the given docblock string for annotations.
       
   162      *
       
   163      * @param string $input The docblock string to parse.
       
   164      * @param string $context The parsing context.
       
   165      * @return array Array of annotations. If no annotations are found, an empty array is returned.
       
   166      */
       
   167     public function parse($input, $context = '')
       
   168     {
       
   169         if (false === $pos = strpos($input, '@')) {
       
   170             return array();
       
   171         }
       
   172 
       
   173         // also parse whatever character is before the @
       
   174         if ($pos > 0) {
       
   175             $pos -= 1;
       
   176         }
       
   177 
       
   178         $this->context = $context;
       
   179         $this->lexer->setInput(trim(substr($input, $pos), '* /'));
       
   180         $this->lexer->moveNext();
       
   181 
       
   182         return $this->Annotations();
       
   183     }
       
   184 
       
   185     /**
       
   186      * Attempts to match the given token with the current lookahead token.
       
   187      * If they match, updates the lookahead token; otherwise raises a syntax error.
       
   188      *
       
   189      * @param int Token type.
       
   190      * @return bool True if tokens match; false otherwise.
       
   191      */
       
   192     private function match($token)
       
   193     {
       
   194         if ( ! $this->lexer->isNextToken($token) ) {
       
   195             $this->syntaxError($this->lexer->getLiteral($token));
       
   196         }
       
   197 
       
   198         return $this->lexer->moveNext();
       
   199     }
       
   200 
       
   201     /**
       
   202      * Attempts to match the current lookahead token with any of the given tokens.
       
   203      *
       
   204      * If any of them matches, this method updates the lookahead token; otherwise
       
   205      * a syntax error is raised.
       
   206      *
       
   207      * @param array $tokens
       
   208      * @return bool
       
   209      */
       
   210     private function matchAny(array $tokens)
       
   211     {
       
   212         if ( ! $this->lexer->isNextTokenAny($tokens)) {
       
   213             $this->syntaxError(implode(' or ', array_map(array($this->lexer, 'getLiteral'), $tokens)));
       
   214         }
       
   215 
       
   216         return $this->lexer->moveNext();
       
   217     }
       
   218 
       
   219     /**
       
   220      * Generates a new syntax error.
       
   221      *
       
   222      * @param string $expected Expected string.
       
   223      * @param array $token Optional token.
       
   224      * @throws SyntaxException
       
   225      */
       
   226     private function syntaxError($expected, $token = null)
       
   227     {
       
   228         if ($token === null) {
       
   229             $token = $this->lexer->lookahead;
       
   230         }
       
   231 
       
   232         $message =  "Expected {$expected}, got ";
       
   233 
       
   234         if ($this->lexer->lookahead === null) {
       
   235             $message .= 'end of string';
       
   236         } else {
       
   237             $message .= "'{$token['value']}' at position {$token['position']}";
       
   238         }
       
   239 
       
   240         if (strlen($this->context)) {
       
   241             $message .= ' in ' . $this->context;
       
   242         }
       
   243 
       
   244         $message .= '.';
       
   245 
       
   246         throw AnnotationException::syntaxError($message);
       
   247     }
       
   248 
       
   249     /**
       
   250      * Attempt to check if a class exists or not. This never goes through the PHP autoloading mechanism
       
   251      * but uses the {@link AnnotationRegistry} to load classes.
       
   252      *
       
   253      * @param string $fqcn
       
   254      * @return boolean
       
   255      */
       
   256     private function classExists($fqcn)
       
   257     {
       
   258         if (isset($this->classExists[$fqcn])) {
       
   259             return $this->classExists[$fqcn];
       
   260         }
       
   261         
       
   262         // first check if the class already exists, maybe loaded through another AnnotationReader
       
   263         if (class_exists($fqcn, false)) {
       
   264             return $this->classExists[$fqcn] = true;
       
   265         }
       
   266 
       
   267         // final check, does this class exist?
       
   268         return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn);
       
   269     }
       
   270 
       
   271     /**
       
   272      * Annotations ::= Annotation {[ "*" ]* [Annotation]}*
       
   273      *
       
   274      * @return array
       
   275      */
       
   276     private function Annotations()
       
   277     {
       
   278         $annotations = array();
       
   279 
       
   280         while (null !== $this->lexer->lookahead) {
       
   281             if (DocLexer::T_AT !== $this->lexer->lookahead['type']) {
       
   282                 $this->lexer->moveNext();
       
   283                 continue;
       
   284             }
       
   285 
       
   286             // make sure the @ is preceded by non-catchable pattern
       
   287             if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) {
       
   288                 $this->lexer->moveNext();
       
   289                 continue;
       
   290             }
       
   291 
       
   292             // make sure the @ is followed by either a namespace separator, or
       
   293             // an identifier token
       
   294             if ((null === $peek = $this->lexer->glimpse())
       
   295                 || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true))
       
   296                 || $peek['position'] !== $this->lexer->lookahead['position'] + 1) {
       
   297                 $this->lexer->moveNext();
       
   298                 continue;
       
   299             }
       
   300 
       
   301             $this->isNestedAnnotation = false;
       
   302             if (false !== $annot = $this->Annotation()) {
       
   303                 $annotations[] = $annot;
       
   304             }
       
   305         }
       
   306 
       
   307         return $annotations;
       
   308     }
       
   309 
       
   310     /**
       
   311      * Annotation     ::= "@" AnnotationName ["(" [Values] ")"]
       
   312      * AnnotationName ::= QualifiedName | SimpleName
       
   313      * QualifiedName  ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName
       
   314      * NameSpacePart  ::= identifier | null | false | true
       
   315      * SimpleName     ::= identifier | null | false | true
       
   316      *
       
   317      * @return mixed False if it is not a valid annotation.
       
   318      */
       
   319     private function Annotation()
       
   320     {
       
   321         $this->match(DocLexer::T_AT);
       
   322 
       
   323         // check if we have an annotation
       
   324         if ($this->lexer->isNextTokenAny(self::$classIdentifiers)) {
       
   325             $this->lexer->moveNext();
       
   326             $name = $this->lexer->token['value'];
       
   327         } else if ($this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) {
       
   328             $name = '';
       
   329         } else {
       
   330             $this->syntaxError('namespace separator or identifier');
       
   331         }
       
   332 
       
   333         while ($this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value']) && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) {
       
   334             $this->match(DocLexer::T_NAMESPACE_SEPARATOR);
       
   335             $this->matchAny(self::$classIdentifiers);
       
   336             $name .= '\\'.$this->lexer->token['value'];
       
   337         }
       
   338 
       
   339         if (strpos($name, ":") !== false) {
       
   340             list ($alias, $name) = explode(':', $name);
       
   341             // If the namespace alias doesnt exist, skip until next annotation
       
   342             if ( ! isset($this->namespaceAliases[$alias])) {
       
   343                 $this->lexer->skipUntil(DocLexer::T_AT);
       
   344                 return false;
       
   345             }
       
   346             $name = $this->namespaceAliases[$alias] . $name;
       
   347         }
       
   348 
       
   349         // only process names which are not fully qualified, yet
       
   350         if ('\\' !== $name[0] && !$this->classExists($name)) {
       
   351             $alias = (false === $pos = strpos($name, '\\'))? $name : substr($name, 0, $pos);
       
   352 
       
   353             if (isset($this->imports[$loweredAlias = strtolower($alias)])) {
       
   354                 if (false !== $pos) {
       
   355                     $name = $this->imports[$loweredAlias].substr($name, $pos);
       
   356                 } else {
       
   357                     $name = $this->imports[$loweredAlias];
       
   358                 }
       
   359             } elseif (isset($this->imports['__DEFAULT__']) && $this->classExists($this->imports['__DEFAULT__'].$name)) {
       
   360                  $name = $this->imports['__DEFAULT__'].$name;
       
   361             } elseif (isset($this->imports['__NAMESPACE__']) && $this->classExists($this->imports['__NAMESPACE__'].'\\'.$name)) {
       
   362                  $name = $this->imports['__NAMESPACE__'].'\\'.$name;
       
   363             } else {
       
   364                 if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) {
       
   365                     return false;
       
   366                 }
       
   367 
       
   368                 throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s was never imported.', $name, $this->context));
       
   369             }
       
   370         }
       
   371 
       
   372         if (!$this->classExists($name)) {
       
   373             throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context));
       
   374         }
       
   375         
       
   376         if (!$this->isAnnotation($name)) {
       
   377             return false;
       
   378         }
       
   379 
       
   380         // Verifies that the annotation class extends any class that contains "Annotation".
       
   381         // This is done to avoid coupling of Doctrine Annotations against other libraries.
       
   382         
       
   383 
       
   384         // at this point, $name contains the fully qualified class name of the
       
   385         // annotation, and it is also guaranteed that this class exists, and
       
   386         // that it is loaded
       
   387 
       
   388         // Next will be nested
       
   389         $this->isNestedAnnotation = true;
       
   390 
       
   391         $values = array();
       
   392         if ($this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) {
       
   393             $this->match(DocLexer::T_OPEN_PARENTHESIS);
       
   394 
       
   395             if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
       
   396                 $values = $this->Values();
       
   397             }
       
   398 
       
   399             $this->match(DocLexer::T_CLOSE_PARENTHESIS);
       
   400         }
       
   401         
       
   402         return $this->newAnnotation($name, $values);
       
   403     }
       
   404     
       
   405     /**
       
   406      * Verify that the found class is actually an annotation.
       
   407      * 
       
   408      * This can be detected through two mechanisms:
       
   409      * 1. Class extends Doctrine\Common\Annotations\Annotation
       
   410      * 2. The class level docblock contains the string "@Annotation"
       
   411      * 
       
   412      * @param string $name
       
   413      * @return bool
       
   414      */
       
   415     private function isAnnotation($name)
       
   416     {
       
   417         if (!isset($this->isAnnotation[$name])) {
       
   418             if (is_subclass_of($name, 'Doctrine\Common\Annotations\Annotation')) {
       
   419                 $this->isAnnotation[$name] = true;
       
   420             } else {
       
   421                 $reflClass = new \ReflectionClass($name);
       
   422                 $this->isAnnotation[$name] = strpos($reflClass->getDocComment(), "@Annotation") !== false;
       
   423             }
       
   424         }
       
   425         return $this->isAnnotation[$name];
       
   426     }
       
   427     
       
   428     private function newAnnotation($name, $values)
       
   429     {
       
   430         if ($this->creationFn !== null) {
       
   431             $fn = $this->creationFn;
       
   432             return $fn($name, $values);
       
   433         }
       
   434         
       
   435         return new $name($values);
       
   436     }
       
   437 
       
   438     /**
       
   439      * Values ::= Array | Value {"," Value}*
       
   440      *
       
   441      * @return array
       
   442      */
       
   443     private function Values()
       
   444     {
       
   445         $values = array();
       
   446 
       
   447         // Handle the case of a single array as value, i.e. @Foo({....})
       
   448         if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
       
   449             $values['value'] = $this->Value();
       
   450             return $values;
       
   451         }
       
   452 
       
   453         $values[] = $this->Value();
       
   454 
       
   455         while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
       
   456             $this->match(DocLexer::T_COMMA);
       
   457             $token = $this->lexer->lookahead;
       
   458             $value = $this->Value();
       
   459 
       
   460             if ( ! is_object($value) && ! is_array($value)) {
       
   461                 $this->syntaxError('Value', $token);
       
   462             }
       
   463 
       
   464             $values[] = $value;
       
   465         }
       
   466 
       
   467         foreach ($values as $k => $value) {
       
   468             if (is_object($value) && $value instanceof \stdClass) {
       
   469                 $values[$value->name] = $value->value;
       
   470             } else if ( ! isset($values['value'])){
       
   471                 $values['value'] = $value;
       
   472             } else {
       
   473                 if ( ! is_array($values['value'])) {
       
   474                     $values['value'] = array($values['value']);
       
   475                 }
       
   476 
       
   477                 $values['value'][] = $value;
       
   478             }
       
   479 
       
   480             unset($values[$k]);
       
   481         }
       
   482 
       
   483         return $values;
       
   484     }
       
   485 
       
   486     /**
       
   487      * Value ::= PlainValue | FieldAssignment
       
   488      *
       
   489      * @return mixed
       
   490      */
       
   491     private function Value()
       
   492     {
       
   493         $peek = $this->lexer->glimpse();
       
   494 
       
   495         if (DocLexer::T_EQUALS === $peek['type']) {
       
   496             return $this->FieldAssignment();
       
   497         }
       
   498 
       
   499         return $this->PlainValue();
       
   500     }
       
   501 
       
   502     /**
       
   503      * PlainValue ::= integer | string | float | boolean | Array | Annotation
       
   504      *
       
   505      * @return mixed
       
   506      */
       
   507     private function PlainValue()
       
   508     {
       
   509         if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
       
   510             return $this->Arrayx();
       
   511         }
       
   512 
       
   513         if ($this->lexer->isNextToken(DocLexer::T_AT)) {
       
   514             return $this->Annotation();
       
   515         }
       
   516 
       
   517         switch ($this->lexer->lookahead['type']) {
       
   518             case DocLexer::T_STRING:
       
   519                 $this->match(DocLexer::T_STRING);
       
   520                 return $this->lexer->token['value'];
       
   521 
       
   522             case DocLexer::T_INTEGER:
       
   523                 $this->match(DocLexer::T_INTEGER);
       
   524                 return (int)$this->lexer->token['value'];
       
   525 
       
   526             case DocLexer::T_FLOAT:
       
   527                 $this->match(DocLexer::T_FLOAT);
       
   528                 return (float)$this->lexer->token['value'];
       
   529 
       
   530             case DocLexer::T_TRUE:
       
   531                 $this->match(DocLexer::T_TRUE);
       
   532                 return true;
       
   533 
       
   534             case DocLexer::T_FALSE:
       
   535                 $this->match(DocLexer::T_FALSE);
       
   536                 return false;
       
   537 
       
   538             case DocLexer::T_NULL:
       
   539                 $this->match(DocLexer::T_NULL);
       
   540                 return null;
       
   541 
       
   542             default:
       
   543                 $this->syntaxError('PlainValue');
       
   544         }
       
   545     }
       
   546 
       
   547     /**
       
   548      * FieldAssignment ::= FieldName "=" PlainValue
       
   549      * FieldName ::= identifier
       
   550      *
       
   551      * @return array
       
   552      */
       
   553     private function FieldAssignment()
       
   554     {
       
   555         $this->match(DocLexer::T_IDENTIFIER);
       
   556         $fieldName = $this->lexer->token['value'];
       
   557 
       
   558         $this->match(DocLexer::T_EQUALS);
       
   559 
       
   560         $item = new \stdClass();
       
   561         $item->name  = $fieldName;
       
   562         $item->value = $this->PlainValue();
       
   563 
       
   564         return $item;
       
   565     }
       
   566 
       
   567     /**
       
   568      * Array ::= "{" ArrayEntry {"," ArrayEntry}* "}"
       
   569      *
       
   570      * @return array
       
   571      */
       
   572     private function Arrayx()
       
   573     {
       
   574         $array = $values = array();
       
   575 
       
   576         $this->match(DocLexer::T_OPEN_CURLY_BRACES);
       
   577         $values[] = $this->ArrayEntry();
       
   578 
       
   579         while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
       
   580             $this->match(DocLexer::T_COMMA);
       
   581             $values[] = $this->ArrayEntry();
       
   582         }
       
   583 
       
   584         $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
       
   585 
       
   586         foreach ($values as $value) {
       
   587             list ($key, $val) = $value;
       
   588 
       
   589             if ($key !== null) {
       
   590                 $array[$key] = $val;
       
   591             } else {
       
   592                 $array[] = $val;
       
   593             }
       
   594         }
       
   595 
       
   596         return $array;
       
   597     }
       
   598 
       
   599     /**
       
   600      * ArrayEntry ::= Value | KeyValuePair
       
   601      * KeyValuePair ::= Key "=" PlainValue
       
   602      * Key ::= string | integer
       
   603      *
       
   604      * @return array
       
   605      */
       
   606     private function ArrayEntry()
       
   607     {
       
   608         $peek = $this->lexer->glimpse();
       
   609 
       
   610         if (DocLexer::T_EQUALS === $peek['type']) {
       
   611             $this->match(
       
   612                 $this->lexer->isNextToken(DocLexer::T_INTEGER) ? DocLexer::T_INTEGER : DocLexer::T_STRING
       
   613             );
       
   614 
       
   615             $key = $this->lexer->token['value'];
       
   616             $this->match(DocLexer::T_EQUALS);
       
   617 
       
   618             return array($key, $this->PlainValue());
       
   619         }
       
   620 
       
   621         return array(null, $this->Value());
       
   622     }
       
   623 }