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