PageRenderTime 41ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/TokenReflection/ReflectionAnnotation.php

http://github.com/Andrewsville/PHP-Token-Reflection
PHP | 484 lines | 294 code | 56 blank | 134 comment | 67 complexity | 888b88eb8fb4ba397f43de33f66127ce MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * PHP Token Reflection
  4. *
  5. * Version 1.4.0
  6. *
  7. * LICENSE
  8. *
  9. * This source file is subject to the new BSD license that is bundled
  10. * with this library in the file LICENSE.md.
  11. *
  12. * @author Ondřej Nešpor
  13. * @author Jaroslav Hanslík
  14. */
  15. namespace TokenReflection;
  16. use TokenReflection\Exception;
  17. /**
  18. * Docblock parser.
  19. */
  20. class ReflectionAnnotation
  21. {
  22. /**
  23. * Main description annotation identifier.
  24. *
  25. * White space at the beginning on purpose.
  26. *
  27. * @var string
  28. */
  29. const SHORT_DESCRIPTION = ' short_description';
  30. /**
  31. * Sub description annotation identifier.
  32. *
  33. * White space at the beginning on purpose.
  34. *
  35. * @var string
  36. */
  37. const LONG_DESCRIPTION = ' long_description';
  38. /**
  39. * Copydoc recursion stack.
  40. *
  41. * Prevents from infinite loops when using the @copydoc annotation.
  42. *
  43. * @var array
  44. */
  45. private static $copydocStack = array();
  46. /**
  47. * List of applied templates.
  48. *
  49. * @var array
  50. */
  51. private $templates = array();
  52. /**
  53. * Parsed annotations.
  54. *
  55. * @var array
  56. */
  57. private $annotations;
  58. /**
  59. * Element docblock.
  60. *
  61. * False if none.
  62. *
  63. * @var string|boolean
  64. */
  65. private $docComment;
  66. /**
  67. * Parent reflection object.
  68. *
  69. * @var \TokenReflection\ReflectionBase
  70. */
  71. private $reflection;
  72. /**
  73. * Constructor.
  74. *
  75. * @param \TokenReflection\ReflectionBase $reflection Parent reflection object
  76. * @param string|boolean $docComment Docblock definition
  77. */
  78. public function __construct(ReflectionBase $reflection, $docComment = false)
  79. {
  80. $this->reflection = $reflection;
  81. $this->docComment = $docComment ?: false;
  82. }
  83. /**
  84. * Returns the docblock.
  85. *
  86. * @return string|boolean
  87. */
  88. public function getDocComment()
  89. {
  90. return $this->docComment;
  91. }
  92. /**
  93. * Returns if the current docblock contains the requrested annotation.
  94. *
  95. * @param string $annotation Annotation name
  96. * @return boolean
  97. */
  98. public function hasAnnotation($annotation)
  99. {
  100. if (null === $this->annotations) {
  101. $this->parse();
  102. }
  103. return isset($this->annotations[$annotation]);
  104. }
  105. /**
  106. * Returns a particular annotation value.
  107. *
  108. * @param string $annotation Annotation name
  109. * @return string|array|null
  110. */
  111. public function getAnnotation($annotation)
  112. {
  113. if (null === $this->annotations) {
  114. $this->parse();
  115. }
  116. return isset($this->annotations[$annotation]) ? $this->annotations[$annotation] : null;
  117. }
  118. /**
  119. * Returns all parsed annotations.
  120. *
  121. * @return array
  122. */
  123. public function getAnnotations()
  124. {
  125. if (null === $this->annotations) {
  126. $this->parse();
  127. }
  128. return $this->annotations;
  129. }
  130. /**
  131. * Sets Docblock templates.
  132. *
  133. * @param array $templates Docblock templates
  134. * @return \TokenReflection\ReflectionAnnotation
  135. * @throws \TokenReflection\Exception\RuntimeException If an invalid annotation template was provided.
  136. */
  137. public function setTemplates(array $templates)
  138. {
  139. foreach ($templates as $template) {
  140. if (!$template instanceof ReflectionAnnotation) {
  141. throw new Exception\RuntimeException(
  142. sprintf(
  143. 'All templates have to be instances of \\TokenReflection\\ReflectionAnnotation; %s given.',
  144. is_object($template) ? get_class($template) : gettype($template)
  145. ),
  146. Exception\RuntimeException::INVALID_ARGUMENT,
  147. $this->reflection
  148. );
  149. }
  150. }
  151. $this->templates = $templates;
  152. return $this;
  153. }
  154. /**
  155. * Parses reflection object documentation.
  156. */
  157. private function parse()
  158. {
  159. $this->annotations = array();
  160. if (false !== $this->docComment) {
  161. // Parse docblock
  162. $name = self::SHORT_DESCRIPTION;
  163. $docblock = trim(
  164. preg_replace(
  165. array(
  166. '~^' . preg_quote(ReflectionElement::DOCBLOCK_TEMPLATE_START, '~') . '~',
  167. '~^' . preg_quote(ReflectionElement::DOCBLOCK_TEMPLATE_END, '~') . '$~',
  168. '~^/\\*\\*~',
  169. '~\\*/$~'
  170. ),
  171. '',
  172. $this->docComment
  173. )
  174. );
  175. foreach (explode("\n", $docblock) as $line) {
  176. $line = preg_replace('~^\\*\\s?~', '', trim($line));
  177. // End of short description
  178. if ('' === $line && self::SHORT_DESCRIPTION === $name) {
  179. $name = self::LONG_DESCRIPTION;
  180. continue;
  181. }
  182. // @annotation
  183. if (preg_match('~^\\s*@([\\S]+)\\s*(.*)~', $line, $matches)) {
  184. $name = $matches[1];
  185. $this->annotations[$name][] = $matches[2];
  186. continue;
  187. }
  188. // Continuation
  189. if (self::SHORT_DESCRIPTION === $name || self::LONG_DESCRIPTION === $name) {
  190. if (!isset($this->annotations[$name])) {
  191. $this->annotations[$name] = $line;
  192. } else {
  193. $this->annotations[$name] .= "\n" . $line;
  194. }
  195. } else {
  196. $this->annotations[$name][count($this->annotations[$name]) - 1] .= "\n" . $line;
  197. }
  198. }
  199. array_walk_recursive($this->annotations, function(&$value) {
  200. // {@*} is a placeholder for */ (phpDocumentor compatibility)
  201. $value = str_replace('{@*}', '*/', $value);
  202. $value = trim($value);
  203. });
  204. }
  205. if ($this->reflection instanceof ReflectionElement) {
  206. // Merge docblock templates
  207. $this->mergeTemplates();
  208. // Copy annotations if the @copydoc tag is present.
  209. if (!empty($this->annotations['copydoc'])) {
  210. $this->copyAnnotation();
  211. }
  212. // Process docblock inheritance for supported reflections
  213. if ($this->reflection instanceof ReflectionClass || $this->reflection instanceof ReflectionMethod || $this->reflection instanceof ReflectionProperty) {
  214. $this->inheritAnnotations();
  215. }
  216. }
  217. }
  218. /**
  219. * Copies annotations if the @copydoc tag is present.
  220. *
  221. * @throws \TokenReflection\Exception\RuntimeException When stuck in an infinite loop when resolving the @copydoc tag.
  222. */
  223. private function copyAnnotation()
  224. {
  225. self::$copydocStack[] = $this->reflection;
  226. $broker = $this->reflection->getBroker();
  227. $parentNames = $this->annotations['copydoc'];
  228. unset($this->annotations['copydoc']);
  229. foreach ($parentNames as $parentName) {
  230. try {
  231. if ($this->reflection instanceof ReflectionClass) {
  232. $parent = $broker->getClass($parentName);
  233. if ($parent instanceof Dummy\ReflectionClass) {
  234. // The class to copy from is not usable
  235. return;
  236. }
  237. } elseif ($this->reflection instanceof ReflectionFunction) {
  238. $parent = $broker->getFunction(rtrim($parentName, '()'));
  239. } elseif ($this->reflection instanceof ReflectionConstant && null === $this->reflection->getDeclaringClassName()) {
  240. $parent = $broker->getConstant($parentName);
  241. } elseif ($this->reflection instanceof ReflectionMethod || $this->reflection instanceof ReflectionProperty || $this->reflection instanceof ReflectionConstant) {
  242. if (false !== strpos($parentName, '::')) {
  243. list($className, $parentName) = explode('::', $parentName, 2);
  244. $class = $broker->getClass($className);
  245. } else {
  246. $class = $this->reflection->getDeclaringClass();
  247. }
  248. if ($class instanceof Dummy\ReflectionClass) {
  249. // The source element class is not usable
  250. return;
  251. }
  252. if ($this->reflection instanceof ReflectionMethod) {
  253. $parent = $class->getMethod(rtrim($parentName, '()'));
  254. } elseif ($this->reflection instanceof ReflectionConstant) {
  255. $parent = $class->getConstantReflection($parentName);
  256. } else {
  257. $parent = $class->getProperty(ltrim($parentName, '$'));
  258. }
  259. }
  260. if (!empty($parent)) {
  261. // Don't get into an infinite recursion loop
  262. if (in_array($parent, self::$copydocStack, true)) {
  263. throw new Exception\RuntimeException('Infinite loop detected when copying annotations using the @copydoc tag.', Exception\RuntimeException::INVALID_ARGUMENT, $this->reflection);
  264. }
  265. self::$copydocStack[] = $parent;
  266. // We can get into an infinite loop here (e.g. when two methods @copydoc from each other)
  267. foreach ($parent->getAnnotations() as $name => $value) {
  268. // Add annotations that are not already present
  269. if (empty($this->annotations[$name])) {
  270. $this->annotations[$name] = $value;
  271. }
  272. }
  273. array_pop(self::$copydocStack);
  274. }
  275. } catch (Exception\BaseException $e) {
  276. // Ignoring links to non existent elements, ...
  277. }
  278. }
  279. array_pop(self::$copydocStack);
  280. }
  281. /**
  282. * Merges templates with the current docblock.
  283. */
  284. private function mergeTemplates()
  285. {
  286. foreach ($this->templates as $index => $template) {
  287. if (0 === $index && $template->getDocComment() === $this->docComment) {
  288. continue;
  289. }
  290. foreach ($template->getAnnotations() as $name => $value) {
  291. if ($name === self::LONG_DESCRIPTION) {
  292. // Long description
  293. if (isset($this->annotations[self::LONG_DESCRIPTION])) {
  294. $this->annotations[self::LONG_DESCRIPTION] = $value . "\n" . $this->annotations[self::LONG_DESCRIPTION];
  295. } else {
  296. $this->annotations[self::LONG_DESCRIPTION] = $value;
  297. }
  298. } elseif ($name !== self::SHORT_DESCRIPTION) {
  299. // Tags; short description is not inherited
  300. if (isset($this->annotations[$name])) {
  301. $this->annotations[$name] = array_merge($this->annotations[$name], $value);
  302. } else {
  303. $this->annotations[$name] = $value;
  304. }
  305. }
  306. }
  307. }
  308. }
  309. /**
  310. * Inherits annotations from parent classes/methods/properties if needed.
  311. *
  312. * @throws \TokenReflection\Exception\RuntimeException If unsupported reflection was used.
  313. */
  314. private function inheritAnnotations()
  315. {
  316. if ($this->reflection instanceof ReflectionClass) {
  317. $declaringClass = $this->reflection;
  318. } elseif ($this->reflection instanceof ReflectionMethod || $this->reflection instanceof ReflectionProperty) {
  319. $declaringClass = $this->reflection->getDeclaringClass();
  320. }
  321. $parents = array_filter(array_merge(array($declaringClass->getParentClass()), $declaringClass->getOwnInterfaces()), function($class) {
  322. return $class instanceof ReflectionClass;
  323. });
  324. // In case of properties and methods, look for a property/method of the same name and return
  325. // and array of such members.
  326. $parentDefinitions = array();
  327. if ($this->reflection instanceof ReflectionProperty) {
  328. $name = $this->reflection->getName();
  329. foreach ($parents as $parent) {
  330. if ($parent->hasProperty($name)) {
  331. $parentDefinitions[] = $parent->getProperty($name);
  332. }
  333. }
  334. $parents = $parentDefinitions;
  335. } elseif ($this->reflection instanceof ReflectionMethod) {
  336. $name = $this->reflection->getName();
  337. foreach ($parents as $parent) {
  338. if ($parent->hasMethod($name)) {
  339. $parentDefinitions[] = $parent->getMethod($name);
  340. }
  341. }
  342. $parents = $parentDefinitions;
  343. }
  344. if (false === $this->docComment) {
  345. // Inherit the entire docblock
  346. foreach ($parents as $parent) {
  347. $annotations = $parent->getAnnotations();
  348. if (!empty($annotations)) {
  349. $this->annotations = $annotations;
  350. break;
  351. }
  352. }
  353. } else {
  354. if (isset($this->annotations[self::LONG_DESCRIPTION]) && false !== stripos($this->annotations[self::LONG_DESCRIPTION], '{@inheritdoc}')) {
  355. // Inherit long description
  356. foreach ($parents as $parent) {
  357. if ($parent->hasAnnotation(self::LONG_DESCRIPTION)) {
  358. $this->annotations[self::LONG_DESCRIPTION] = str_ireplace(
  359. '{@inheritdoc}',
  360. $parent->getAnnotation(self::LONG_DESCRIPTION),
  361. $this->annotations[self::LONG_DESCRIPTION]
  362. );
  363. break;
  364. }
  365. }
  366. $this->annotations[self::LONG_DESCRIPTION] = str_ireplace('{@inheritdoc}', '', $this->annotations[self::LONG_DESCRIPTION]);
  367. }
  368. if (isset($this->annotations[self::SHORT_DESCRIPTION]) && false !== stripos($this->annotations[self::SHORT_DESCRIPTION], '{@inheritdoc}')) {
  369. // Inherit short description
  370. foreach ($parents as $parent) {
  371. if ($parent->hasAnnotation(self::SHORT_DESCRIPTION)) {
  372. $this->annotations[self::SHORT_DESCRIPTION] = str_ireplace(
  373. '{@inheritdoc}',
  374. $parent->getAnnotation(self::SHORT_DESCRIPTION),
  375. $this->annotations[self::SHORT_DESCRIPTION]
  376. );
  377. break;
  378. }
  379. }
  380. $this->annotations[self::SHORT_DESCRIPTION] = str_ireplace('{@inheritdoc}', '', $this->annotations[self::SHORT_DESCRIPTION]);
  381. }
  382. }
  383. // In case of properties check if we need and can inherit the data type
  384. if ($this->reflection instanceof ReflectionProperty && empty($this->annotations['var'])) {
  385. foreach ($parents as $parent) {
  386. if ($parent->hasAnnotation('var')) {
  387. $this->annotations['var'] = $parent->getAnnotation('var');
  388. break;
  389. }
  390. }
  391. }
  392. if ($this->reflection instanceof ReflectionMethod) {
  393. if (0 !== $this->reflection->getNumberOfParameters() && (empty($this->annotations['param']) || count($this->annotations['param']) < $this->reflection->getNumberOfParameters())) {
  394. // In case of methods check if we need and can inherit parameter descriptions
  395. $params = isset($this->annotations['param']) ? $this->annotations['param'] : array();
  396. $complete = false;
  397. foreach ($parents as $parent) {
  398. if ($parent->hasAnnotation('param')) {
  399. $parentParams = array_slice($parent->getAnnotation('param'), count($params));
  400. while (!empty($parentParams) && !$complete) {
  401. array_push($params, array_shift($parentParams));
  402. if (count($params) === $this->reflection->getNumberOfParameters()) {
  403. $complete = true;
  404. }
  405. }
  406. }
  407. if ($complete) {
  408. break;
  409. }
  410. }
  411. if (!empty($params)) {
  412. $this->annotations['param'] = $params;
  413. }
  414. }
  415. // And check if we need and can inherit the return and throws value
  416. foreach (array('return', 'throws') as $paramName) {
  417. if (!isset($this->annotations[$paramName])) {
  418. foreach ($parents as $parent) {
  419. if ($parent->hasAnnotation($paramName)) {
  420. $this->annotations[$paramName] = $parent->getAnnotation($paramName);
  421. break;
  422. }
  423. }
  424. }
  425. }
  426. }
  427. }
  428. }