ReflectionParameter   F
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 511
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 17

Test Coverage

Coverage 95.63%

Importance

Changes 0
Metric Value
wmc 65
lcom 2
cbo 17
dl 0
loc 511
ccs 153
cts 160
cp 0.9563
rs 3.2
c 0
b 0
f 0

36 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A createFromClassNameAndMethod() 0 9 1
A createFromClassInstanceAndMethod() 0 9 1
A createFromClosure() 0 5 1
B createFromSpec() 0 20 7
A __toString() 0 4 1
A createFromNode() 0 16 1
B parseDefaultValueNode() 0 37 7
A findParentClassDeclaringConstant() 0 17 3
A getName() 0 5 1
A getDeclaringFunction() 0 4 1
A getDeclaringClass() 0 8 2
A isOptional() 0 4 2
A isDefaultValueAvailable() 0 4 1
A getDefaultValue() 0 6 1
A allowsNull() 0 16 4
A getDocBlockTypeStrings() 0 10 2
A getDocBlockTypes() 0 4 1
A getPosition() 0 4 1
A getType() 0 14 3
A hasType() 0 4 1
A setType() 0 4 1
A removeType() 0 4 1
A isArray() 0 4 1
A isCallable() 0 4 1
A isVariadic() 0 4 1
A isPassedByReference() 0 4 1
A canBePassedByValue() 0 4 1
A isDefaultValueConstant() 0 6 1
A getDefaultValueConstantName() 0 9 2
A getClass() 0 18 3
A getClassName() 0 32 5
A __clone() 0 4 1
A getStartColumn() 0 4 1
A getEndColumn() 0 4 1
A getAst() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ReflectionParameter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ReflectionParameter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Roave\BetterReflection\Reflection;
6
7
use Closure;
8
use Exception;
9
use InvalidArgumentException;
10
use LogicException;
11
use OutOfBoundsException;
12
use phpDocumentor\Reflection\Type;
13
use PhpParser\Node;
14
use PhpParser\Node\NullableType;
15
use PhpParser\Node\Param as ParamNode;
16
use PhpParser\Node\Stmt\Namespace_;
17
use Roave\BetterReflection\NodeCompiler\CompileNodeToValue;
18
use Roave\BetterReflection\NodeCompiler\CompilerContext;
19
use Roave\BetterReflection\Reflection\Exception\Uncloneable;
20
use Roave\BetterReflection\Reflection\StringCast\ReflectionParameterStringCast;
21
use Roave\BetterReflection\Reflector\ClassReflector;
22
use Roave\BetterReflection\Reflector\Reflector;
23
use Roave\BetterReflection\TypesFinder\FindParameterType;
24
use Roave\BetterReflection\Util\CalculateReflectionColum;
25
use RuntimeException;
26
use function count;
27
use function get_class;
28
use function in_array;
29
use function is_array;
30
use function is_object;
31
use function is_string;
32
use function sprintf;
33
use function strtolower;
34
35
class ReflectionParameter
36
{
37
    /** @var ParamNode */
38
    private $node;
39
40
    /** @var Namespace_|null */
41
    private $declaringNamespace;
42
43
    /** @var ReflectionFunctionAbstract */
44
    private $function;
45
46
    /** @var int */
47
    private $parameterIndex;
48
49
    /** @var mixed */
50
    private $defaultValue;
51
52
    /** @var bool */
53
    private $isDefaultValueConstant = false;
54
55
    /** @var string|null */
56
    private $defaultValueConstantName;
57
58
    /** @var Reflector */
59
    private $reflector;
60
61
    private function __construct()
62 58
    {
63
    }
64 58
65
    /**
66 1
     * Create a reflection of a parameter using a class name
67
     *
68 1
     * @throws OutOfBoundsException
69
     */
70
    public static function createFromClassNameAndMethod(
71
        string $className,
72
        string $methodName,
73
        string $parameterName
74
    ) : self {
75
        return ReflectionClass::createFromName($className)
76 2
            ->getMethod($methodName)
77
            ->getParameter($parameterName);
78
    }
79
80
    /**
81 2
     * Create a reflection of a parameter using an instance
82 2
     *
83 2
     * @param object $instance
84
     *
85
     * @throws OutOfBoundsException
86
     */
87
    public static function createFromClassInstanceAndMethod(
88
        $instance,
89
        string $methodName,
90
        string $parameterName
91
    ) : self {
92
        return ReflectionClass::createFromInstance($instance)
93 2
            ->getMethod($methodName)
94
            ->getParameter($parameterName);
95
    }
96
97
    /**
98 2
     * Create a reflection of a parameter using a closure
99 2
     */
100 2
    public static function createFromClosure(Closure $closure, string $parameterName) : ReflectionParameter
101
    {
102
        return ReflectionFunction::createFromClosure($closure)
103
            ->getParameter($parameterName);
104
    }
105
106 2
    /**
107
     * Create the parameter from the given spec. Possible $spec parameters are:
108 2
     *
109 2
     *  - [$instance, 'method']
110
     *  - ['Foo', 'bar']
111
     *  - ['foo']
112
     *  - [function () {}]
113
     *
114
     * @param object[]|string[]|string|Closure $spec
115
     *
116
     * @throws Exception
117
     * @throws InvalidArgumentException
118
     */
119
    public static function createFromSpec($spec, string $parameterName) : self
120
    {
121
        if (is_array($spec) && count($spec) === 2 && is_string($spec[1])) {
122
            if (is_object($spec[0])) {
123
                return self::createFromClassInstanceAndMethod($spec[0], $spec[1], $parameterName);
124
            }
125 5
126
            return self::createFromClassNameAndMethod($spec[0], $spec[1], $parameterName);
127 5
        }
128 2
129 1
        if (is_string($spec)) {
130
            return ReflectionFunction::createFromName($spec)->getParameter($parameterName);
131
        }
132 1
133
        if ($spec instanceof Closure) {
134
            return self::createFromClosure($spec, $parameterName);
135 3
        }
136 1
137
        throw new InvalidArgumentException('Could not create reflection from the spec given');
138
    }
139 2
140 1
    public function __toString() : string
141
    {
142
        return ReflectionParameterStringCast::toString($this);
143 1
    }
144
145
    /**
146 1
     * @internal
147
     *
148 1
     * @param ParamNode       $node               Node has to be processed by the PhpParser\NodeVisitor\NameResolver
149
     * @param Namespace_|null $declaringNamespace namespace of the declaring function/method
150
     */
151
    public static function createFromNode(
152
        Reflector $reflector,
153
        ParamNode $node,
154
        ?Namespace_ $declaringNamespace,
155
        ReflectionFunctionAbstract $function,
156
        int $parameterIndex
157 58
    ) : self {
158
        $param                     = new self();
159
        $param->reflector          = $reflector;
160
        $param->node               = $node;
161
        $param->declaringNamespace = $declaringNamespace;
162
        $param->function           = $function;
163
        $param->parameterIndex     = $parameterIndex;
164 58
165 58
        return $param;
166 58
    }
167 58
168 58
    private function parseDefaultValueNode() : void
169 58
    {
170
        if (! $this->isDefaultValueAvailable()) {
171 58
            throw new LogicException('This parameter does not have a default value available');
172
        }
173
174 13
        $defaultValueNode = $this->node->default;
175
176 13
        if ($defaultValueNode instanceof Node\Expr\ClassConstFetch) {
177 1
            /** @var Node\Name $defaultValueNode->class */
178
            $className = $defaultValueNode->class->toString();
179
180 12
            if ($className === 'self' || $className === 'static') {
181
                /** @var Node\Identifier $defaultValueNode->name */
182 12
                $constantName = $defaultValueNode->name->name;
183
                $className    = $this->findParentClassDeclaringConstant($constantName);
184 2
            }
185
186 2
            $this->isDefaultValueConstant = true;
187
            /** @var Node\Identifier $defaultValueNode->name */
188 2
            $this->defaultValueConstantName = $className . '::' . $defaultValueNode->name->name;
189 2
        }
190
191
        if ($defaultValueNode instanceof Node\Expr\ConstFetch
192 2
            && ! in_array(strtolower($defaultValueNode->name->parts[0]), ['true', 'false', 'null'], true)) {
193
            $this->isDefaultValueConstant   = true;
194 2
            $this->defaultValueConstantName = $defaultValueNode->name->parts[0];
195
            $this->defaultValue             = null;
196
197 12
            return;
198 12
        }
199 1
200 1
        $this->defaultValue = (new CompileNodeToValue())->__invoke(
201 1
            $defaultValueNode,
0 ignored issues
show
Bug introduced by James Titcumb
It seems like $defaultValueNode defined by $this->node->default on line 174 can be null; however, Roave\BetterReflection\N...NodeToValue::__invoke() 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...
202
            new CompilerContext($this->reflector, $this->getDeclaringClass())
203 1
        );
204
    }
205
206 12
    /**
207 12
     * @throws LogicException
208 12
     */
209
    private function findParentClassDeclaringConstant(string $constantName) : string
210 12
    {
211
        /** @var ReflectionMethod $method */
212
        $method = $this->function;
213
        $class  = $method->getDeclaringClass();
214
215 2
        do {
216
            if ($class->hasConstant($constantName)) {
217
                return $class->getName();
218 2
            }
219 2
220
            $class = $class->getParentClass();
221
        } while ($class);
222 2
223 2
        // note: this code is theoretically unreachable, so don't expect any coverage on it
224
        throw new LogicException(sprintf('Failed to find parent class of constant "%s".', $constantName));
225
    }
226
227
    /**
228
     * Get the name of the parameter.
229
     */
230
    public function getName() : string
231
    {
232
        /** @var string $this->node->var->name */
233
        return $this->node->var->name;
234
    }
235
236 58
    /**
237
     * Get the function (or method) that declared this parameter.
238
     */
239 58
    public function getDeclaringFunction() : ReflectionFunctionAbstract
240
    {
241
        return $this->function;
242
    }
243
244
    /**
245 1
     * Get the class from the method that this parameter belongs to, if it
246
     * exists.
247 1
     *
248
     * This will return null if the declaring function is not a method.
249
     */
250
    public function getDeclaringClass() : ?ReflectionClass
251
    {
252
        if ($this->function instanceof ReflectionMethod) {
253
            return $this->function->getDeclaringClass();
254
        }
255
256 16
        return null;
257
    }
258 16
259 15
    /**
260
     * Is the parameter optional?
261
     *
262 1
     * Note this is distinct from "isDefaultValueAvailable" because you can have
263
     * a default value, but the parameter not be optional. In the example, the
264
     * $foo parameter isOptional() == false, but isDefaultValueAvailable == true
265
     *
266
     * @example someMethod($foo = 'foo', $bar)
267
     */
268
    public function isOptional() : bool
269
    {
270
        return ((bool) $this->node->isOptional) || $this->isVariadic();
271
    }
272
273
    /**
274 3
     * Does the parameter have a default, regardless of whether it is optional.
275
     *
276 3
     * Note this is distinct from "isOptional" because you can have
277
     * a default value, but the parameter not be optional. In the example, the
278
     * $foo parameter isOptional() == false, but isDefaultValueAvailable == true
279
     *
280
     * @example someMethod($foo = 'foo', $bar)
281
     */
282
    public function isDefaultValueAvailable() : bool
283
    {
284
        return $this->node->default !== null;
285
    }
286
287
    /**
288 28
     * Get the default value of the parameter.
289
     *
290 28
     * @return mixed
291
     *
292
     * @throws LogicException
293
     */
294
    public function getDefaultValue()
295
    {
296
        $this->parseDefaultValueNode();
297
298
        return $this->defaultValue;
299
    }
300 11
301
    /**
302 11
     * Does this method allow null for a parameter?
303
     */
304 10
    public function allowsNull() : bool
305
    {
306
        if (! $this->hasType()) {
307
            return true;
308
        }
309
310 19
        if ($this->node->type instanceof NullableType) {
311
            return true;
312 19
        }
313 1
314
        if (! $this->isDefaultValueAvailable()) {
315
            return false;
316 19
        }
317 3
318
        return $this->getDefaultValue() === null;
319
    }
320 16
321 15
    /**
322
     * Get the DocBlock type hints as an array of strings.
323
     *
324 2
     * @return string[]
325
     */
326
    public function getDocBlockTypeStrings() : array
327
    {
328
        $stringTypes = [];
329
330
        foreach ($this->getDocBlockTypes() as $type) {
331
            $stringTypes[] = (string) $type;
332 1
        }
333
334 1
        return $stringTypes;
335
    }
336 1
337 1
    /**
338
     * Get the types defined in the DocBlocks. This returns an array because
339
     * the parameter may have multiple (compound) types specified (for example
340 1
     * when you type hint pipe-separated "string|null", in which case this
341
     * would return an array of Type objects, one for string, one for null.
342
     *
343
     * @see getTypeHint()
344
     *
345
     * @return Type[]
346
     */
347
    public function getDocBlockTypes() : array
348
    {
349
        return (new FindParameterType())->__invoke($this->function, $this->declaringNamespace, $this->node);
350
    }
351
352
    /**
353 2
     * Find the position of the parameter, left to right, starting at zero.
354
     */
355 2
    public function getPosition() : int
356
    {
357
        return $this->parameterIndex;
358
    }
359
360
    /**
361 2
     * Get the ReflectionType instance representing the type declaration for
362
     * this parameter
363 2
     *
364
     * (note: this has nothing to do with DocBlocks).
365
     */
366
    public function getType() : ?ReflectionType
367
    {
368
        $type = $this->node->type;
369
370
        if ($type === null) {
371
            return null;
372 20
        }
373
374 20
        if ($type instanceof NullableType) {
375
            $type = $type->type;
376 20
        }
377 2
378
        return ReflectionType::createFromTypeAndReflector((string) $type, $this->allowsNull(), $this->reflector);
379
    }
380 18
381 3
    /**
382
     * Does this parameter have a type declaration?
383
     *
384 18
     * (note: this has nothing to do with DocBlocks).
385
     */
386
    public function hasType() : bool
387
    {
388
        return $this->node->type !== null;
389
    }
390
391
    /**
392 22
     * Set the parameter type declaration.
393
     */
394 22
    public function setType(string $newParameterType) : void
395
    {
396
        $this->node->type = new Node\Name($newParameterType);
397
    }
398
399
    /**
400 1
     * Remove the parameter type declaration completely.
401
     */
402 1
    public function removeType() : void
403 1
    {
404
        $this->node->type = null;
405
    }
406
407
    /**
408 1
     * Is this parameter an array?
409
     */
410 1
    public function isArray() : bool
411 1
    {
412
        return strtolower((string) $this->getType()) === 'array';
413
    }
414
415
    /**
416 1
     * Is this parameter a callable?
417
     */
418 1
    public function isCallable() : bool
419
    {
420
        return strtolower((string) $this->getType()) === 'callable';
421
    }
422
423
    /**
424 1
     * Is this parameter a variadic (denoted by ...$param).
425
     */
426 1
    public function isVariadic() : bool
427
    {
428
        return $this->node->variadic;
429
    }
430
431
    /**
432 4
     * Is this parameter passed by reference (denoted by &$param).
433
     */
434 4
    public function isPassedByReference() : bool
435
    {
436
        return $this->node->byRef;
437
    }
438
439
    public function canBePassedByValue() : bool
440 2
    {
441
        return ! $this->isPassedByReference();
442 2
    }
443
444
    public function isDefaultValueConstant() : bool
445 1
    {
446
        $this->parseDefaultValueNode();
447 1
448
        return $this->isDefaultValueConstant;
449
    }
450 2
451
    /**
452 2
     * @throws LogicException
453
     */
454 2
    public function getDefaultValueConstantName() : string
455
    {
456
        $this->parseDefaultValueNode();
457
        if (! $this->isDefaultValueConstant()) {
458
            throw new LogicException('This parameter is not a constant default value, so cannot have a constant name');
459
        }
460 2
461
        return $this->defaultValueConstantName;
462 2
    }
463 2
464 1
    /**
465
     * Gets a ReflectionClass for the type hint (returns null if not a class)
466
     *
467 2
     * @throws RuntimeException
468
     */
469
    public function getClass() : ?ReflectionClass
470
    {
471
        $className = $this->getClassName();
472
473
        if ($className === null) {
474
            return null;
475 4
        }
476
477 4
        if (! $this->reflector instanceof ClassReflector) {
478
            throw new RuntimeException(sprintf(
479 4
                'Unable to reflect class type because we were not given a "%s", but a "%s" instead',
480 2
                ClassReflector::class,
481
                get_class($this->reflector)
482
            ));
483 3
        }
484
485
        return $this->reflector->reflect($className);
486
    }
487
488
    private function getClassName() : ?string
489
    {
490
        if (! $this->hasType()) {
491 3
            return null;
492
        }
493
494 4
        /** @var ReflectionType $type */
495
        $type     = $this->getType();
496 4
        $typeHint = (string) $type;
497 1
498
        if ($typeHint === 'self') {
499
            /** @var ReflectionClass $declaringClass */
500
            $declaringClass = $this->getDeclaringClass();
501 4
502 4
            return $declaringClass->getName();
503
        }
504 4
505
        if ($typeHint === 'parent') {
506 1
            /** @var ReflectionClass $declaringClass */
507
            $declaringClass = $this->getDeclaringClass();
508 1
            /** @var ReflectionClass $parentClass */
509
            $parentClass = $declaringClass->getParentClass();
510
511 3
            return $parentClass->getName();
512
        }
513 1
514
        if ($type->isBuiltin()) {
515 1
            return null;
516
        }
517 1
518
        return $typeHint;
519
    }
520 2
521 2
    /**
522
     * {@inheritdoc}
523
     *
524 1
     * @throws Uncloneable
525
     */
526
    public function __clone()
527
    {
528
        throw Uncloneable::fromClass(self::class);
529
    }
530
531
    public function getStartColumn() : int
532 1
    {
533
        return CalculateReflectionColum::getStartColumn($this->function->getLocatedSource()->getSource(), $this->node);
534 1
    }
535
536
    public function getEndColumn() : int
537 4
    {
538
        return CalculateReflectionColum::getEndColumn($this->function->getLocatedSource()->getSource(), $this->node);
539 4
    }
540
541
    public function getAst() : ParamNode
542 4
    {
543
        return $this->node;
544 4
    }
545
}
546