DocParser.php 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  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. // fully qualified names must start with a \
  304. $originalName = $name;
  305. if ('\\' !== $name[0]) {
  306. $alias = (false === $pos = strpos($name, '\\'))? $name : substr($name, 0, $pos);
  307. if (isset($this->imports[$loweredAlias = strtolower($alias)])) {
  308. if (false !== $pos) {
  309. $name = $this->imports[$loweredAlias].substr($name, $pos);
  310. } else {
  311. $name = $this->imports[$loweredAlias];
  312. }
  313. } elseif (isset($this->imports['__DEFAULT__']) && $this->classExists($this->imports['__DEFAULT__'].$name)) {
  314. $name = $this->imports['__DEFAULT__'].$name;
  315. } elseif (isset($this->imports['__NAMESPACE__']) && $this->classExists($this->imports['__NAMESPACE__'].'\\'.$name)) {
  316. $name = $this->imports['__NAMESPACE__'].'\\'.$name;
  317. } elseif (!$this->classExists($name)) {
  318. if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) {
  319. return false;
  320. }
  321. 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));
  322. }
  323. }
  324. if (!$this->classExists($name)) {
  325. throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context));
  326. }
  327. if (!$this->isAnnotation($name)) {
  328. return false;
  329. }
  330. // Verifies that the annotation class extends any class that contains "Annotation".
  331. // This is done to avoid coupling of Doctrine Annotations against other libraries.
  332. // at this point, $name contains the fully qualified class name of the
  333. // annotation, and it is also guaranteed that this class exists, and
  334. // that it is loaded
  335. // Next will be nested
  336. $this->isNestedAnnotation = true;
  337. $values = array();
  338. if ($this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) {
  339. $this->match(DocLexer::T_OPEN_PARENTHESIS);
  340. if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
  341. $values = $this->Values();
  342. }
  343. $this->match(DocLexer::T_CLOSE_PARENTHESIS);
  344. }
  345. return $this->newAnnotation($name, $values);
  346. }
  347. /**
  348. * Verify that the found class is actually an annotation.
  349. *
  350. * This can be detected through two mechanisms:
  351. * 1. Class extends Doctrine\Common\Annotations\Annotation
  352. * 2. The class level docblock contains the string "@Annotation"
  353. *
  354. * @param string $name
  355. * @return bool
  356. */
  357. private function isAnnotation($name)
  358. {
  359. if (!isset($this->isAnnotation[$name])) {
  360. if (is_subclass_of($name, 'Doctrine\Common\Annotations\Annotation')) {
  361. $this->isAnnotation[$name] = true;
  362. } else {
  363. $reflClass = new \ReflectionClass($name);
  364. $this->isAnnotation[$name] = strpos($reflClass->getDocComment(), "@Annotation") !== false;
  365. }
  366. }
  367. return $this->isAnnotation[$name];
  368. }
  369. private function newAnnotation($name, $values)
  370. {
  371. if ($this->creationFn !== null) {
  372. $fn = $this->creationFn;
  373. return $fn($name, $values);
  374. }
  375. return new $name($values);
  376. }
  377. /**
  378. * Values ::= Array | Value {"," Value}*
  379. *
  380. * @return array
  381. */
  382. private function Values()
  383. {
  384. $values = array();
  385. // Handle the case of a single array as value, i.e. @Foo({....})
  386. if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
  387. $values['value'] = $this->Value();
  388. return $values;
  389. }
  390. $values[] = $this->Value();
  391. while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
  392. $this->match(DocLexer::T_COMMA);
  393. $token = $this->lexer->lookahead;
  394. $value = $this->Value();
  395. if ( ! is_object($value) && ! is_array($value)) {
  396. $this->syntaxError('Value', $token);
  397. }
  398. $values[] = $value;
  399. }
  400. foreach ($values as $k => $value) {
  401. if (is_object($value) && $value instanceof \stdClass) {
  402. $values[$value->name] = $value->value;
  403. } else if ( ! isset($values['value'])){
  404. $values['value'] = $value;
  405. } else {
  406. if ( ! is_array($values['value'])) {
  407. $values['value'] = array($values['value']);
  408. }
  409. $values['value'][] = $value;
  410. }
  411. unset($values[$k]);
  412. }
  413. return $values;
  414. }
  415. /**
  416. * Value ::= PlainValue | FieldAssignment
  417. *
  418. * @return mixed
  419. */
  420. private function Value()
  421. {
  422. $peek = $this->lexer->glimpse();
  423. if (DocLexer::T_EQUALS === $peek['type']) {
  424. return $this->FieldAssignment();
  425. }
  426. return $this->PlainValue();
  427. }
  428. /**
  429. * PlainValue ::= integer | string | float | boolean | Array | Annotation
  430. *
  431. * @return mixed
  432. */
  433. private function PlainValue()
  434. {
  435. if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
  436. return $this->Arrayx();
  437. }
  438. if ($this->lexer->isNextToken(DocLexer::T_AT)) {
  439. return $this->Annotation();
  440. }
  441. switch ($this->lexer->lookahead['type']) {
  442. case DocLexer::T_STRING:
  443. $this->match(DocLexer::T_STRING);
  444. return $this->lexer->token['value'];
  445. case DocLexer::T_INTEGER:
  446. $this->match(DocLexer::T_INTEGER);
  447. return (int)$this->lexer->token['value'];
  448. case DocLexer::T_FLOAT:
  449. $this->match(DocLexer::T_FLOAT);
  450. return (float)$this->lexer->token['value'];
  451. case DocLexer::T_TRUE:
  452. $this->match(DocLexer::T_TRUE);
  453. return true;
  454. case DocLexer::T_FALSE:
  455. $this->match(DocLexer::T_FALSE);
  456. return false;
  457. case DocLexer::T_NULL:
  458. $this->match(DocLexer::T_NULL);
  459. return null;
  460. default:
  461. $this->syntaxError('PlainValue');
  462. }
  463. }
  464. /**
  465. * FieldAssignment ::= FieldName "=" PlainValue
  466. * FieldName ::= identifier
  467. *
  468. * @return array
  469. */
  470. private function FieldAssignment()
  471. {
  472. $this->match(DocLexer::T_IDENTIFIER);
  473. $fieldName = $this->lexer->token['value'];
  474. $this->match(DocLexer::T_EQUALS);
  475. $item = new \stdClass();
  476. $item->name = $fieldName;
  477. $item->value = $this->PlainValue();
  478. return $item;
  479. }
  480. /**
  481. * Array ::= "{" ArrayEntry {"," ArrayEntry}* "}"
  482. *
  483. * @return array
  484. */
  485. private function Arrayx()
  486. {
  487. $array = $values = array();
  488. $this->match(DocLexer::T_OPEN_CURLY_BRACES);
  489. $values[] = $this->ArrayEntry();
  490. while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
  491. $this->match(DocLexer::T_COMMA);
  492. $values[] = $this->ArrayEntry();
  493. }
  494. $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
  495. foreach ($values as $value) {
  496. list ($key, $val) = $value;
  497. if ($key !== null) {
  498. $array[$key] = $val;
  499. } else {
  500. $array[] = $val;
  501. }
  502. }
  503. return $array;
  504. }
  505. /**
  506. * ArrayEntry ::= Value | KeyValuePair
  507. * KeyValuePair ::= Key "=" PlainValue
  508. * Key ::= string | integer
  509. *
  510. * @return array
  511. */
  512. private function ArrayEntry()
  513. {
  514. $peek = $this->lexer->glimpse();
  515. if (DocLexer::T_EQUALS === $peek['type']) {
  516. $this->match(
  517. $this->lexer->isNextToken(DocLexer::T_INTEGER) ? DocLexer::T_INTEGER : DocLexer::T_STRING
  518. );
  519. $key = $this->lexer->token['value'];
  520. $this->match(DocLexer::T_EQUALS);
  521. return array($key, $this->PlainValue());
  522. }
  523. return array(null, $this->Value());
  524. }
  525. }