1<?php 2/** 3 * This file is part of phpDocumentor. 4 * 5 * For the full copyright and license information, please view the LICENSE 6 * file that was distributed with this source code. 7 * 8 * @copyright 2010-2015 Mike van Riel<mike@phpdoc.org> 9 * @license http://www.opensource.org/licenses/mit-license.php MIT 10 * @link http://phpdoc.org 11 */ 12 13namespace phpDocumentor\Reflection\DocBlock; 14 15use phpDocumentor\Reflection\DocBlock\Tags\Factory\StaticMethod; 16use phpDocumentor\Reflection\DocBlock\Tags\Generic; 17use phpDocumentor\Reflection\FqsenResolver; 18use phpDocumentor\Reflection\Types\Context as TypeContext; 19use Webmozart\Assert\Assert; 20 21/** 22 * Creates a Tag object given the contents of a tag. 23 * 24 * This Factory is capable of determining the appropriate class for a tag and instantiate it using its `create` 25 * factory method. The `create` factory method of a Tag can have a variable number of arguments; this way you can 26 * pass the dependencies that you need to construct a tag object. 27 * 28 * > Important: each parameter in addition to the body variable for the `create` method must default to null, otherwise 29 * > it violates the constraint with the interface; it is recommended to use the {@see Assert::notNull()} method to 30 * > verify that a dependency is actually passed. 31 * 32 * This Factory also features a Service Locator component that is used to pass the right dependencies to the 33 * `create` method of a tag; each dependency should be registered as a service or as a parameter. 34 * 35 * When you want to use a Tag of your own with custom handling you need to call the `registerTagHandler` method, pass 36 * the name of the tag and a Fully Qualified Class Name pointing to a class that implements the Tag interface. 37 */ 38final class StandardTagFactory implements TagFactory 39{ 40 /** PCRE regular expression matching a tag name. */ 41 const REGEX_TAGNAME = '[\w\-\_\\\\]+'; 42 43 /** 44 * @var string[] An array with a tag as a key, and an FQCN to a class that handles it as an array value. 45 */ 46 private $tagHandlerMappings = [ 47 'author' => '\phpDocumentor\Reflection\DocBlock\Tags\Author', 48 'covers' => '\phpDocumentor\Reflection\DocBlock\Tags\Covers', 49 'deprecated' => '\phpDocumentor\Reflection\DocBlock\Tags\Deprecated', 50 // 'example' => '\phpDocumentor\Reflection\DocBlock\Tags\Example', 51 'link' => '\phpDocumentor\Reflection\DocBlock\Tags\Link', 52 'method' => '\phpDocumentor\Reflection\DocBlock\Tags\Method', 53 'param' => '\phpDocumentor\Reflection\DocBlock\Tags\Param', 54 'property-read' => '\phpDocumentor\Reflection\DocBlock\Tags\PropertyRead', 55 'property' => '\phpDocumentor\Reflection\DocBlock\Tags\Property', 56 'property-write' => '\phpDocumentor\Reflection\DocBlock\Tags\PropertyWrite', 57 'return' => '\phpDocumentor\Reflection\DocBlock\Tags\Return_', 58 'see' => '\phpDocumentor\Reflection\DocBlock\Tags\See', 59 'since' => '\phpDocumentor\Reflection\DocBlock\Tags\Since', 60 'source' => '\phpDocumentor\Reflection\DocBlock\Tags\Source', 61 'throw' => '\phpDocumentor\Reflection\DocBlock\Tags\Throws', 62 'throws' => '\phpDocumentor\Reflection\DocBlock\Tags\Throws', 63 'uses' => '\phpDocumentor\Reflection\DocBlock\Tags\Uses', 64 'var' => '\phpDocumentor\Reflection\DocBlock\Tags\Var_', 65 'version' => '\phpDocumentor\Reflection\DocBlock\Tags\Version' 66 ]; 67 68 /** 69 * @var \ReflectionParameter[][] a lazy-loading cache containing parameters for each tagHandler that has been used. 70 */ 71 private $tagHandlerParameterCache = []; 72 73 /** 74 * @var FqsenResolver 75 */ 76 private $fqsenResolver; 77 78 /** 79 * @var mixed[] an array representing a simple Service Locator where we can store parameters and 80 * services that can be inserted into the Factory Methods of Tag Handlers. 81 */ 82 private $serviceLocator = []; 83 84 /** 85 * Initialize this tag factory with the means to resolve an FQSEN and optionally a list of tag handlers. 86 * 87 * If no tag handlers are provided than the default list in the {@see self::$tagHandlerMappings} property 88 * is used. 89 * 90 * @param FqsenResolver $fqsenResolver 91 * @param string[] $tagHandlers 92 * 93 * @see self::registerTagHandler() to add a new tag handler to the existing default list. 94 */ 95 public function __construct(FqsenResolver $fqsenResolver, array $tagHandlers = null) 96 { 97 $this->fqsenResolver = $fqsenResolver; 98 if ($tagHandlers !== null) { 99 $this->tagHandlerMappings = $tagHandlers; 100 } 101 102 $this->addService($fqsenResolver, FqsenResolver::class); 103 } 104 105 /** 106 * {@inheritDoc} 107 */ 108 public function create($tagLine, TypeContext $context = null) 109 { 110 if (! $context) { 111 $context = new TypeContext(''); 112 } 113 114 list($tagName, $tagBody) = $this->extractTagParts($tagLine); 115 116 if ($tagBody !== '' && $tagBody[0] === '[') { 117 throw new \InvalidArgumentException( 118 'The tag "' . $tagLine . '" does not seem to be wellformed, please check it for errors' 119 ); 120 } 121 122 return $this->createTag($tagBody, $tagName, $context); 123 } 124 125 /** 126 * {@inheritDoc} 127 */ 128 public function addParameter($name, $value) 129 { 130 $this->serviceLocator[$name] = $value; 131 } 132 133 /** 134 * {@inheritDoc} 135 */ 136 public function addService($service, $alias = null) 137 { 138 $this->serviceLocator[$alias ?: get_class($service)] = $service; 139 } 140 141 /** 142 * {@inheritDoc} 143 */ 144 public function registerTagHandler($tagName, $handler) 145 { 146 Assert::stringNotEmpty($tagName); 147 Assert::stringNotEmpty($handler); 148 Assert::classExists($handler); 149 Assert::implementsInterface($handler, StaticMethod::class); 150 151 if (strpos($tagName, '\\') && $tagName[0] !== '\\') { 152 throw new \InvalidArgumentException( 153 'A namespaced tag must have a leading backslash as it must be fully qualified' 154 ); 155 } 156 157 $this->tagHandlerMappings[$tagName] = $handler; 158 } 159 160 /** 161 * Extracts all components for a tag. 162 * 163 * @param string $tagLine 164 * 165 * @return string[] 166 */ 167 private function extractTagParts($tagLine) 168 { 169 $matches = []; 170 if (! preg_match('/^@(' . self::REGEX_TAGNAME . ')(?:\s*([^\s].*)|$)/us', $tagLine, $matches)) { 171 throw new \InvalidArgumentException( 172 'The tag "' . $tagLine . '" does not seem to be wellformed, please check it for errors' 173 ); 174 } 175 176 if (count($matches) < 3) { 177 $matches[] = ''; 178 } 179 180 return array_slice($matches, 1); 181 } 182 183 /** 184 * Creates a new tag object with the given name and body or returns null if the tag name was recognized but the 185 * body was invalid. 186 * 187 * @param string $body 188 * @param string $name 189 * @param TypeContext $context 190 * 191 * @return Tag|null 192 */ 193 private function createTag($body, $name, TypeContext $context) 194 { 195 $handlerClassName = $this->findHandlerClassName($name, $context); 196 $arguments = $this->getArgumentsForParametersFromWiring( 197 $this->fetchParametersForHandlerFactoryMethod($handlerClassName), 198 $this->getServiceLocatorWithDynamicParameters($context, $name, $body) 199 ); 200 201 return call_user_func_array([$handlerClassName, 'create'], $arguments); 202 } 203 204 /** 205 * Determines the Fully Qualified Class Name of the Factory or Tag (containing a Factory Method `create`). 206 * 207 * @param string $tagName 208 * @param TypeContext $context 209 * 210 * @return string 211 */ 212 private function findHandlerClassName($tagName, TypeContext $context) 213 { 214 $handlerClassName = Generic::class; 215 if (isset($this->tagHandlerMappings[$tagName])) { 216 $handlerClassName = $this->tagHandlerMappings[$tagName]; 217 } elseif ($this->isAnnotation($tagName)) { 218 // TODO: Annotation support is planned for a later stage and as such is disabled for now 219 // $tagName = (string)$this->fqsenResolver->resolve($tagName, $context); 220 // if (isset($this->annotationMappings[$tagName])) { 221 // $handlerClassName = $this->annotationMappings[$tagName]; 222 // } 223 } 224 225 return $handlerClassName; 226 } 227 228 /** 229 * Retrieves the arguments that need to be passed to the Factory Method with the given Parameters. 230 * 231 * @param \ReflectionParameter[] $parameters 232 * @param mixed[] $locator 233 * 234 * @return mixed[] A series of values that can be passed to the Factory Method of the tag whose parameters 235 * is provided with this method. 236 */ 237 private function getArgumentsForParametersFromWiring($parameters, $locator) 238 { 239 $arguments = []; 240 foreach ($parameters as $index => $parameter) { 241 $typeHint = $parameter->getClass() ? $parameter->getClass()->getName() : null; 242 if (isset($locator[$typeHint])) { 243 $arguments[] = $locator[$typeHint]; 244 continue; 245 } 246 247 $parameterName = $parameter->getName(); 248 if (isset($locator[$parameterName])) { 249 $arguments[] = $locator[$parameterName]; 250 continue; 251 } 252 253 $arguments[] = null; 254 } 255 256 return $arguments; 257 } 258 259 /** 260 * Retrieves a series of ReflectionParameter objects for the static 'create' method of the given 261 * tag handler class name. 262 * 263 * @param string $handlerClassName 264 * 265 * @return \ReflectionParameter[] 266 */ 267 private function fetchParametersForHandlerFactoryMethod($handlerClassName) 268 { 269 if (! isset($this->tagHandlerParameterCache[$handlerClassName])) { 270 $methodReflection = new \ReflectionMethod($handlerClassName, 'create'); 271 $this->tagHandlerParameterCache[$handlerClassName] = $methodReflection->getParameters(); 272 } 273 274 return $this->tagHandlerParameterCache[$handlerClassName]; 275 } 276 277 /** 278 * Returns a copy of this class' Service Locator with added dynamic parameters, such as the tag's name, body and 279 * Context. 280 * 281 * @param TypeContext $context The Context (namespace and aliasses) that may be passed and is used to resolve FQSENs. 282 * @param string $tagName The name of the tag that may be passed onto the factory method of the Tag class. 283 * @param string $tagBody The body of the tag that may be passed onto the factory method of the Tag class. 284 * 285 * @return mixed[] 286 */ 287 private function getServiceLocatorWithDynamicParameters(TypeContext $context, $tagName, $tagBody) 288 { 289 $locator = array_merge( 290 $this->serviceLocator, 291 [ 292 'name' => $tagName, 293 'body' => $tagBody, 294 TypeContext::class => $context 295 ] 296 ); 297 298 return $locator; 299 } 300 301 /** 302 * Returns whether the given tag belongs to an annotation. 303 * 304 * @param string $tagContent 305 * 306 * @todo this method should be populated once we implement Annotation notation support. 307 * 308 * @return bool 309 */ 310 private function isAnnotation($tagContent) 311 { 312 // 1. Contains a namespace separator 313 // 2. Contains parenthesis 314 // 3. Is present in a list of known annotations (make the algorithm smart by first checking is the last part 315 // of the annotation class name matches the found tag name 316 317 return false; 318 } 319} 320