Completed
Push — master ( 9cf5c1...719e81 )
by Jaap
01:46
created

TypeResolver::isNullableType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
295
    }
296
}
297