DocParser.php 18KB

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