Completed
Push — master ( b73a2b...b21eaf )
by Jaap
25:34 queued 15:36
created

src/DocBlock/StandardTagFactory.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

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
The doc-type array<class-string<StaticMethod>> could not be parsed: Unknown type name "class-string" at position 6. (view supported doc-types)

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.

Loading history...
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