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