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;
14
15use phpDocumentor\Reflection\Types\Array_;
16use phpDocumentor\Reflection\Types\Compound;
17use phpDocumentor\Reflection\Types\Context;
18use phpDocumentor\Reflection\Types\Iterable_;
19use phpDocumentor\Reflection\Types\Nullable;
20use phpDocumentor\Reflection\Types\Object_;
21
22final class TypeResolver
23{
24    /** @var string Definition of the ARRAY operator for types */
25    const OPERATOR_ARRAY = '[]';
26
27    /** @var string Definition of the NAMESPACE operator in PHP */
28    const OPERATOR_NAMESPACE = '\\';
29
30    /** @var string[] List of recognized keywords and unto which Value Object they map */
31    private $keywords = array(
32        'string' => Types\String_::class,
33        'int' => Types\Integer::class,
34        'integer' => Types\Integer::class,
35        'bool' => Types\Boolean::class,
36        'boolean' => Types\Boolean::class,
37        'float' => Types\Float_::class,
38        'double' => Types\Float_::class,
39        'object' => Object_::class,
40        'mixed' => Types\Mixed_::class,
41        'array' => Array_::class,
42        'resource' => Types\Resource_::class,
43        'void' => Types\Void_::class,
44        'null' => Types\Null_::class,
45        'scalar' => Types\Scalar::class,
46        'callback' => Types\Callable_::class,
47        'callable' => Types\Callable_::class,
48        'false' => Types\Boolean::class,
49        'true' => Types\Boolean::class,
50        'self' => Types\Self_::class,
51        '$this' => Types\This::class,
52        'static' => Types\Static_::class,
53        'parent' => Types\Parent_::class,
54        'iterable' => Iterable_::class,
55    );
56
57    /** @var FqsenResolver */
58    private $fqsenResolver;
59
60    /**
61     * Initializes this TypeResolver with the means to create and resolve Fqsen objects.
62     *
63     * @param FqsenResolver $fqsenResolver
64     */
65    public function __construct(FqsenResolver $fqsenResolver = null)
66    {
67        $this->fqsenResolver = $fqsenResolver ?: new FqsenResolver();
68    }
69
70    /**
71     * Analyzes the given type and returns the FQCN variant.
72     *
73     * When a type is provided this method checks whether it is not a keyword or
74     * Fully Qualified Class Name. If so it will use the given namespace and
75     * aliases to expand the type to a FQCN representation.
76     *
77     * This method only works as expected if the namespace and aliases are set;
78     * no dynamic reflection is being performed here.
79     *
80     * @param string $type     The relative or absolute type.
81     * @param Context $context
82     *
83     * @uses Context::getNamespace()        to determine with what to prefix the type name.
84     * @uses Context::getNamespaceAliases() to check whether the first part of the relative type name should not be
85     *     replaced with another namespace.
86     *
87     * @return Type|null
88     */
89    public function resolve($type, Context $context = null)
90    {
91        if (!is_string($type)) {
92            throw new \InvalidArgumentException(
93                'Attempted to resolve type but it appeared not to be a string, received: ' . var_export($type, true)
94            );
95        }
96
97        $type = trim($type);
98        if (!$type) {
99            throw new \InvalidArgumentException('Attempted to resolve "' . $type . '" but it appears to be empty');
100        }
101
102        if ($context === null) {
103            $context = new Context('');
104        }
105
106        switch (true) {
107            case $this->isNullableType($type):
108                return $this->resolveNullableType($type, $context);
109            case $this->isKeyword($type):
110                return $this->resolveKeyword($type);
111            case ($this->isCompoundType($type)):
112                return $this->resolveCompoundType($type, $context);
113            case $this->isTypedArray($type):
114                return $this->resolveTypedArray($type, $context);
115            case $this->isFqsen($type):
116                return $this->resolveTypedObject($type);
117            case $this->isPartialStructuralElementName($type):
118                return $this->resolveTypedObject($type, $context);
119            // @codeCoverageIgnoreStart
120            default:
121                // I haven't got the foggiest how the logic would come here but added this as a defense.
122                throw new \RuntimeException(
123                    'Unable to resolve type "' . $type . '", there is no known method to resolve it'
124                );
125        }
126        // @codeCoverageIgnoreEnd
127    }
128
129    /**
130     * Adds a keyword to the list of Keywords and associates it with a specific Value Object.
131     *
132     * @param string $keyword
133     * @param string $typeClassName
134     *
135     * @return void
136     */
137    public function addKeyword($keyword, $typeClassName)
138    {
139        if (!class_exists($typeClassName)) {
140            throw new \InvalidArgumentException(
141                'The Value Object that needs to be created with a keyword "' . $keyword . '" must be an existing class'
142                . ' but we could not find the class ' . $typeClassName
143            );
144        }
145
146        if (!in_array(Type::class, class_implements($typeClassName))) {
147            throw new \InvalidArgumentException(
148                'The class "' . $typeClassName . '" must implement the interface "phpDocumentor\Reflection\Type"'
149            );
150        }
151
152        $this->keywords[$keyword] = $typeClassName;
153    }
154
155    /**
156     * Detects whether the given type represents an array.
157     *
158     * @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
159     *
160     * @return bool
161     */
162    private function isTypedArray($type)
163    {
164        return substr($type, -2) === self::OPERATOR_ARRAY;
165    }
166
167    /**
168     * Detects whether the given type represents a PHPDoc keyword.
169     *
170     * @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
171     *
172     * @return bool
173     */
174    private function isKeyword($type)
175    {
176        return in_array(strtolower($type), array_keys($this->keywords), true);
177    }
178
179    /**
180     * Detects whether the given type represents a relative structural element name.
181     *
182     * @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
183     *
184     * @return bool
185     */
186    private function isPartialStructuralElementName($type)
187    {
188        return ($type[0] !== self::OPERATOR_NAMESPACE) && !$this->isKeyword($type);
189    }
190
191    /**
192     * Tests whether the given type is a Fully Qualified Structural Element Name.
193     *
194     * @param string $type
195     *
196     * @return bool
197     */
198    private function isFqsen($type)
199    {
200        return strpos($type, self::OPERATOR_NAMESPACE) === 0;
201    }
202
203    /**
204     * Tests whether the given type is a compound type (i.e. `string|int`).
205     *
206     * @param string $type
207     *
208     * @return bool
209     */
210    private function isCompoundType($type)
211    {
212        return strpos($type, '|') !== false;
213    }
214
215    /**
216     * Test whether the given type is a nullable type (i.e. `?string`)
217     *
218     * @param string $type
219     *
220     * @return bool
221     */
222    private function isNullableType($type)
223    {
224        return $type[0] === '?';
225    }
226
227    /**
228     * Resolves the given typed array string (i.e. `string[]`) into an Array object with the right types set.
229     *
230     * @param string $type
231     * @param Context $context
232     *
233     * @return Array_
234     */
235    private function resolveTypedArray($type, Context $context)
236    {
237        return new Array_($this->resolve(substr($type, 0, -2), $context));
238    }
239
240    /**
241     * Resolves the given keyword (such as `string`) into a Type object representing that keyword.
242     *
243     * @param string $type
244     *
245     * @return Type
246     */
247    private function resolveKeyword($type)
248    {
249        $className = $this->keywords[strtolower($type)];
250
251        return new $className();
252    }
253
254    /**
255     * Resolves the given FQSEN string into an FQSEN object.
256     *
257     * @param string $type
258     * @param Context|null $context
259     *
260     * @return Object_
261     */
262    private function resolveTypedObject($type, Context $context = null)
263    {
264        return new Object_($this->fqsenResolver->resolve($type, $context));
265    }
266
267    /**
268     * Resolves a compound type (i.e. `string|int`) into the appropriate Type objects or FQSEN.
269     *
270     * @param string $type
271     * @param Context $context
272     *
273     * @return Compound
274     */
275    private function resolveCompoundType($type, Context $context)
276    {
277        $types = [];
278
279        foreach (explode('|', $type) as $part) {
280            $types[] = $this->resolve($part, $context);
281        }
282
283        return new Compound($types);
284    }
285
286    /**
287     * Resolve nullable types (i.e. `?string`) into a Nullable type wrapper
288     *
289     * @param string $type
290     * @param Context $context
291     *
292     * @return Nullable
293     */
294    private function resolveNullableType($type, Context $context)
295    {
296        return new Nullable($this->resolve(ltrim($type, '?'), $context));
297    }
298}
299