/TokenReflection/ReflectionAnnotation.php
PHP | 484 lines | 294 code | 56 blank | 134 comment | 67 complexity | 888b88eb8fb4ba397f43de33f66127ce MD5 | raw file
Possible License(s): BSD-3-Clause
- <?php
- /**
- * PHP Token Reflection
- *
- * Version 1.4.0
- *
- * LICENSE
- *
- * This source file is subject to the new BSD license that is bundled
- * with this library in the file LICENSE.md.
- *
- * @author Ondřej Nešpor
- * @author Jaroslav Hanslík
- */
- namespace TokenReflection;
- use TokenReflection\Exception;
- /**
- * Docblock parser.
- */
- class ReflectionAnnotation
- {
- /**
- * Main description annotation identifier.
- *
- * White space at the beginning on purpose.
- *
- * @var string
- */
- const SHORT_DESCRIPTION = ' short_description';
- /**
- * Sub description annotation identifier.
- *
- * White space at the beginning on purpose.
- *
- * @var string
- */
- const LONG_DESCRIPTION = ' long_description';
- /**
- * Copydoc recursion stack.
- *
- * Prevents from infinite loops when using the @copydoc annotation.
- *
- * @var array
- */
- private static $copydocStack = array();
- /**
- * List of applied templates.
- *
- * @var array
- */
- private $templates = array();
- /**
- * Parsed annotations.
- *
- * @var array
- */
- private $annotations;
- /**
- * Element docblock.
- *
- * False if none.
- *
- * @var string|boolean
- */
- private $docComment;
- /**
- * Parent reflection object.
- *
- * @var \TokenReflection\ReflectionBase
- */
- private $reflection;
- /**
- * Constructor.
- *
- * @param \TokenReflection\ReflectionBase $reflection Parent reflection object
- * @param string|boolean $docComment Docblock definition
- */
- public function __construct(ReflectionBase $reflection, $docComment = false)
- {
- $this->reflection = $reflection;
- $this->docComment = $docComment ?: false;
- }
- /**
- * Returns the docblock.
- *
- * @return string|boolean
- */
- public function getDocComment()
- {
- return $this->docComment;
- }
- /**
- * Returns if the current docblock contains the requrested annotation.
- *
- * @param string $annotation Annotation name
- * @return boolean
- */
- public function hasAnnotation($annotation)
- {
- if (null === $this->annotations) {
- $this->parse();
- }
- return isset($this->annotations[$annotation]);
- }
- /**
- * Returns a particular annotation value.
- *
- * @param string $annotation Annotation name
- * @return string|array|null
- */
- public function getAnnotation($annotation)
- {
- if (null === $this->annotations) {
- $this->parse();
- }
- return isset($this->annotations[$annotation]) ? $this->annotations[$annotation] : null;
- }
- /**
- * Returns all parsed annotations.
- *
- * @return array
- */
- public function getAnnotations()
- {
- if (null === $this->annotations) {
- $this->parse();
- }
- return $this->annotations;
- }
- /**
- * Sets Docblock templates.
- *
- * @param array $templates Docblock templates
- * @return \TokenReflection\ReflectionAnnotation
- * @throws \TokenReflection\Exception\RuntimeException If an invalid annotation template was provided.
- */
- public function setTemplates(array $templates)
- {
- foreach ($templates as $template) {
- if (!$template instanceof ReflectionAnnotation) {
- throw new Exception\RuntimeException(
- sprintf(
- 'All templates have to be instances of \\TokenReflection\\ReflectionAnnotation; %s given.',
- is_object($template) ? get_class($template) : gettype($template)
- ),
- Exception\RuntimeException::INVALID_ARGUMENT,
- $this->reflection
- );
- }
- }
- $this->templates = $templates;
- return $this;
- }
- /**
- * Parses reflection object documentation.
- */
- private function parse()
- {
- $this->annotations = array();
- if (false !== $this->docComment) {
- // Parse docblock
- $name = self::SHORT_DESCRIPTION;
- $docblock = trim(
- preg_replace(
- array(
- '~^' . preg_quote(ReflectionElement::DOCBLOCK_TEMPLATE_START, '~') . '~',
- '~^' . preg_quote(ReflectionElement::DOCBLOCK_TEMPLATE_END, '~') . '$~',
- '~^/\\*\\*~',
- '~\\*/$~'
- ),
- '',
- $this->docComment
- )
- );
- foreach (explode("\n", $docblock) as $line) {
- $line = preg_replace('~^\\*\\s?~', '', trim($line));
- // End of short description
- if ('' === $line && self::SHORT_DESCRIPTION === $name) {
- $name = self::LONG_DESCRIPTION;
- continue;
- }
- // @annotation
- if (preg_match('~^\\s*@([\\S]+)\\s*(.*)~', $line, $matches)) {
- $name = $matches[1];
- $this->annotations[$name][] = $matches[2];
- continue;
- }
- // Continuation
- if (self::SHORT_DESCRIPTION === $name || self::LONG_DESCRIPTION === $name) {
- if (!isset($this->annotations[$name])) {
- $this->annotations[$name] = $line;
- } else {
- $this->annotations[$name] .= "\n" . $line;
- }
- } else {
- $this->annotations[$name][count($this->annotations[$name]) - 1] .= "\n" . $line;
- }
- }
- array_walk_recursive($this->annotations, function(&$value) {
- // {@*} is a placeholder for */ (phpDocumentor compatibility)
- $value = str_replace('{@*}', '*/', $value);
- $value = trim($value);
- });
- }
- if ($this->reflection instanceof ReflectionElement) {
- // Merge docblock templates
- $this->mergeTemplates();
- // Copy annotations if the @copydoc tag is present.
- if (!empty($this->annotations['copydoc'])) {
- $this->copyAnnotation();
- }
- // Process docblock inheritance for supported reflections
- if ($this->reflection instanceof ReflectionClass || $this->reflection instanceof ReflectionMethod || $this->reflection instanceof ReflectionProperty) {
- $this->inheritAnnotations();
- }
- }
- }
- /**
- * Copies annotations if the @copydoc tag is present.
- *
- * @throws \TokenReflection\Exception\RuntimeException When stuck in an infinite loop when resolving the @copydoc tag.
- */
- private function copyAnnotation()
- {
- self::$copydocStack[] = $this->reflection;
- $broker = $this->reflection->getBroker();
- $parentNames = $this->annotations['copydoc'];
- unset($this->annotations['copydoc']);
- foreach ($parentNames as $parentName) {
- try {
- if ($this->reflection instanceof ReflectionClass) {
- $parent = $broker->getClass($parentName);
- if ($parent instanceof Dummy\ReflectionClass) {
- // The class to copy from is not usable
- return;
- }
- } elseif ($this->reflection instanceof ReflectionFunction) {
- $parent = $broker->getFunction(rtrim($parentName, '()'));
- } elseif ($this->reflection instanceof ReflectionConstant && null === $this->reflection->getDeclaringClassName()) {
- $parent = $broker->getConstant($parentName);
- } elseif ($this->reflection instanceof ReflectionMethod || $this->reflection instanceof ReflectionProperty || $this->reflection instanceof ReflectionConstant) {
- if (false !== strpos($parentName, '::')) {
- list($className, $parentName) = explode('::', $parentName, 2);
- $class = $broker->getClass($className);
- } else {
- $class = $this->reflection->getDeclaringClass();
- }
- if ($class instanceof Dummy\ReflectionClass) {
- // The source element class is not usable
- return;
- }
- if ($this->reflection instanceof ReflectionMethod) {
- $parent = $class->getMethod(rtrim($parentName, '()'));
- } elseif ($this->reflection instanceof ReflectionConstant) {
- $parent = $class->getConstantReflection($parentName);
- } else {
- $parent = $class->getProperty(ltrim($parentName, '$'));
- }
- }
- if (!empty($parent)) {
- // Don't get into an infinite recursion loop
- if (in_array($parent, self::$copydocStack, true)) {
- throw new Exception\RuntimeException('Infinite loop detected when copying annotations using the @copydoc tag.', Exception\RuntimeException::INVALID_ARGUMENT, $this->reflection);
- }
- self::$copydocStack[] = $parent;
- // We can get into an infinite loop here (e.g. when two methods @copydoc from each other)
- foreach ($parent->getAnnotations() as $name => $value) {
- // Add annotations that are not already present
- if (empty($this->annotations[$name])) {
- $this->annotations[$name] = $value;
- }
- }
- array_pop(self::$copydocStack);
- }
- } catch (Exception\BaseException $e) {
- // Ignoring links to non existent elements, ...
- }
- }
- array_pop(self::$copydocStack);
- }
- /**
- * Merges templates with the current docblock.
- */
- private function mergeTemplates()
- {
- foreach ($this->templates as $index => $template) {
- if (0 === $index && $template->getDocComment() === $this->docComment) {
- continue;
- }
- foreach ($template->getAnnotations() as $name => $value) {
- if ($name === self::LONG_DESCRIPTION) {
- // Long description
- if (isset($this->annotations[self::LONG_DESCRIPTION])) {
- $this->annotations[self::LONG_DESCRIPTION] = $value . "\n" . $this->annotations[self::LONG_DESCRIPTION];
- } else {
- $this->annotations[self::LONG_DESCRIPTION] = $value;
- }
- } elseif ($name !== self::SHORT_DESCRIPTION) {
- // Tags; short description is not inherited
- if (isset($this->annotations[$name])) {
- $this->annotations[$name] = array_merge($this->annotations[$name], $value);
- } else {
- $this->annotations[$name] = $value;
- }
- }
- }
- }
- }
- /**
- * Inherits annotations from parent classes/methods/properties if needed.
- *
- * @throws \TokenReflection\Exception\RuntimeException If unsupported reflection was used.
- */
- private function inheritAnnotations()
- {
- if ($this->reflection instanceof ReflectionClass) {
- $declaringClass = $this->reflection;
- } elseif ($this->reflection instanceof ReflectionMethod || $this->reflection instanceof ReflectionProperty) {
- $declaringClass = $this->reflection->getDeclaringClass();
- }
- $parents = array_filter(array_merge(array($declaringClass->getParentClass()), $declaringClass->getOwnInterfaces()), function($class) {
- return $class instanceof ReflectionClass;
- });
- // In case of properties and methods, look for a property/method of the same name and return
- // and array of such members.
- $parentDefinitions = array();
- if ($this->reflection instanceof ReflectionProperty) {
- $name = $this->reflection->getName();
- foreach ($parents as $parent) {
- if ($parent->hasProperty($name)) {
- $parentDefinitions[] = $parent->getProperty($name);
- }
- }
- $parents = $parentDefinitions;
- } elseif ($this->reflection instanceof ReflectionMethod) {
- $name = $this->reflection->getName();
- foreach ($parents as $parent) {
- if ($parent->hasMethod($name)) {
- $parentDefinitions[] = $parent->getMethod($name);
- }
- }
- $parents = $parentDefinitions;
- }
- if (false === $this->docComment) {
- // Inherit the entire docblock
- foreach ($parents as $parent) {
- $annotations = $parent->getAnnotations();
- if (!empty($annotations)) {
- $this->annotations = $annotations;
- break;
- }
- }
- } else {
- if (isset($this->annotations[self::LONG_DESCRIPTION]) && false !== stripos($this->annotations[self::LONG_DESCRIPTION], '{@inheritdoc}')) {
- // Inherit long description
- foreach ($parents as $parent) {
- if ($parent->hasAnnotation(self::LONG_DESCRIPTION)) {
- $this->annotations[self::LONG_DESCRIPTION] = str_ireplace(
- '{@inheritdoc}',
- $parent->getAnnotation(self::LONG_DESCRIPTION),
- $this->annotations[self::LONG_DESCRIPTION]
- );
- break;
- }
- }
- $this->annotations[self::LONG_DESCRIPTION] = str_ireplace('{@inheritdoc}', '', $this->annotations[self::LONG_DESCRIPTION]);
- }
- if (isset($this->annotations[self::SHORT_DESCRIPTION]) && false !== stripos($this->annotations[self::SHORT_DESCRIPTION], '{@inheritdoc}')) {
- // Inherit short description
- foreach ($parents as $parent) {
- if ($parent->hasAnnotation(self::SHORT_DESCRIPTION)) {
- $this->annotations[self::SHORT_DESCRIPTION] = str_ireplace(
- '{@inheritdoc}',
- $parent->getAnnotation(self::SHORT_DESCRIPTION),
- $this->annotations[self::SHORT_DESCRIPTION]
- );
- break;
- }
- }
- $this->annotations[self::SHORT_DESCRIPTION] = str_ireplace('{@inheritdoc}', '', $this->annotations[self::SHORT_DESCRIPTION]);
- }
- }
- // In case of properties check if we need and can inherit the data type
- if ($this->reflection instanceof ReflectionProperty && empty($this->annotations['var'])) {
- foreach ($parents as $parent) {
- if ($parent->hasAnnotation('var')) {
- $this->annotations['var'] = $parent->getAnnotation('var');
- break;
- }
- }
- }
- if ($this->reflection instanceof ReflectionMethod) {
- if (0 !== $this->reflection->getNumberOfParameters() && (empty($this->annotations['param']) || count($this->annotations['param']) < $this->reflection->getNumberOfParameters())) {
- // In case of methods check if we need and can inherit parameter descriptions
- $params = isset($this->annotations['param']) ? $this->annotations['param'] : array();
- $complete = false;
- foreach ($parents as $parent) {
- if ($parent->hasAnnotation('param')) {
- $parentParams = array_slice($parent->getAnnotation('param'), count($params));
- while (!empty($parentParams) && !$complete) {
- array_push($params, array_shift($parentParams));
- if (count($params) === $this->reflection->getNumberOfParameters()) {
- $complete = true;
- }
- }
- }
- if ($complete) {
- break;
- }
- }
- if (!empty($params)) {
- $this->annotations['param'] = $params;
- }
- }
- // And check if we need and can inherit the return and throws value
- foreach (array('return', 'throws') as $paramName) {
- if (!isset($this->annotations[$paramName])) {
- foreach ($parents as $parent) {
- if ($parent->hasAnnotation($paramName)) {
- $this->annotations[$paramName] = $parent->getAnnotation($paramName);
- break;
- }
- }
- }
- }
- }
- }
- }