Completed
Pull Request — master (#82)
by Loren
05:32
created

ReflectionParameter::isDefaultValueSet()   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
c 0
b 0
f 0
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
/**
3
 * Parser Reflection API
4
 *
5
 * @copyright Copyright 2015, Lisachenko Alexander <[email protected]>
6
 *
7
 * This source file is subject to the license that is bundled
8
 * with this source code in the file LICENSE.
9
 */
10
11
namespace Go\ParserReflection;
12
13
use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait;
14
use Go\ParserReflection\ValueResolver\NodeExpressionResolver;
15
use PhpParser\Node\Name;
16
use PhpParser\Node\NullableType;
17
use PhpParser\Node\Param;
18
use ReflectionParameter as BaseReflectionParameter;
19
20
/**
21
 * AST-based reflection for method/function parameter
22
 */
23
class ReflectionParameter extends BaseReflectionParameter implements ReflectionInterface
24
{
25
    use InternalPropertiesEmulationTrait;
26
27
    /**
28
     * Reflection function or method
29
     *
30
     * @var \ReflectionFunctionAbstract
31
     */
32
    private $declaringFunction;
33
34
    /**
35
     * Stores the default value for node (if present)
36
     *
37
     * @var mixed
38
     */
39
    private $defaultValue = null;
40
41
    /**
42
     * Whether or not default value is constant
43
     *
44
     * @var bool
45
     */
46
    private $isDefaultValueConstant = false;
47
48
    /**
49
     * Name of the constant of default value
50
     *
51
     * @var string
52
     */
53
    private $defaultValueConstantName;
54
55
    /**
56
     * Index of parameter in the list
57
     *
58
     * @var int
59
     */
60
    private $parameterIndex = 0;
61
62
    /**
63
     * Concrete parameter node
64
     *
65
     * @var Param
66
     */
67
    private $parameterNode;
68
69
    /**
70
     * Initializes a reflection for the property
71
     *
72
     * @param string|array $unusedFunctionName Name of the function/method
73
     * @param string $parameterName Name of the parameter to reflect
74
     * @param Param $parameterNode Parameter definition node
75
     * @param int $parameterIndex Index of parameter
76
     * @param \ReflectionFunctionAbstract $declaringFunction
77
     */
78 122
    public function __construct(
79
        $unusedFunctionName,
80
        $parameterName,
81
        Param $parameterNode = null,
82
        $parameterIndex = 0,
83
        \ReflectionFunctionAbstract $declaringFunction = null
84
    ) {
85
        // Let's unset original read-only property to have a control over it via __get
86 122
        unset($this->name);
87
88 122
        $this->parameterNode     = $parameterNode;
89 122
        $this->parameterIndex    = $parameterIndex;
90 122
        $this->declaringFunction = $declaringFunction;
91
92 122
        if ($this->isDefaultValueSet()) {
93 60
            if ($declaringFunction instanceof \ReflectionMethod) {
94 30
                $context = $declaringFunction->getDeclaringClass();
95
            } else {
96 30
                $context = $declaringFunction;
97
            };
98
99 60
            $expressionSolver = new NodeExpressionResolver($context);
100 60
            $expressionSolver->process($this->parameterNode->default);
101 60
            $this->defaultValue             = $expressionSolver->getValue();
102 60
            $this->isDefaultValueConstant   = $expressionSolver->isConstant();
103 60
            $this->defaultValueConstantName = $expressionSolver->getConstantName();
104
        }
105 122
    }
106
107
    /**
108
     * Returns an AST-node for parameter
109
     *
110
     * @return Param
111
     */
112
    public function getNode()
113
    {
114
        return $this->parameterNode;
115
    }
116
117
    /**
118
     * Emulating original behaviour of reflection
119
     */
120 1
    public function ___debugInfo()
121
    {
122
        return array(
123 1
            'name' => $this->parameterNode->name,
124
        );
125
    }
126
127
    /**
128
     * Returns string representation of this parameter.
129
     *
130
     * @return string
131
     */
132 70
    public function __toString()
133
    {
134 70
        $parameterType   = $this->getType();
135 70
        $isOptional      = $this->isOptional();
136 70
        $hasDefaultValue = $this->isDefaultValueAvailable();
137 70
        $defaultValue    = '';
138 70
        if ($hasDefaultValue) {
139 25
            $defaultValue = $this->getDefaultValue();
140 25
            if (is_string($defaultValue) && strlen($defaultValue) > 15) {
141 8
                $defaultValue = substr($defaultValue, 0, 15) . '...';
142
            }
143
            /* @see https://3v4l.org/DJOEb for behaviour changes */
144 25
            if (is_double($defaultValue) && fmod($defaultValue, 1.0) === 0.0) {
145 8
                $defaultValue = (int)$defaultValue;
146
            }
147
148 25
            $defaultValue = str_replace('\\\\', '\\', var_export($defaultValue, true));
149
        }
150
151 70
        return sprintf(
152 70
            'Parameter #%d [ %s %s%s%s$%s%s ]',
153 70
            $this->parameterIndex,
154 70
            $isOptional ? '<optional>' : '<required>',
155 70
            $parameterType ? ReflectionType::convertToDisplayType($parameterType) . ' ' : '',
156 70
            $this->isVariadic() ? '...' : '',
157 70
            $this->isPassedByReference() ? '&' : '',
158 70
            $this->getName(),
159 70
            ($isOptional && $hasDefaultValue) ? (' = ' . $defaultValue) : ''
160
        );
161
    }
162
163
    /**
164
     * {@inheritDoc}
165
     */
166 71
    public function allowsNull()
167
    {
168
        // Allow builtin types to override
169 71
        if ($this->parameterNode->getAttribute('prohibit_null', false)) {
170 7
            return false;
171
        }
172
173
        // Enable 7.1 nullable types support
174 64
        if ($this->parameterNode->type instanceof NullableType) {
175 12
            return true;
176
        }
177
178 52
        $hasDefaultNull = $this->isDefaultValueAvailable() && $this->getDefaultValue() === null;
179 52
        if ($hasDefaultNull) {
180 11
            return true;
181
        }
182
183 46
        return !isset($this->parameterNode->type);
184
    }
185
186
    /**
187
     * {@inheritDoc}
188
     */
189 18
    public function canBePassedByValue()
190
    {
191 18
        return !$this->isPassedByReference();
192
    }
193
194
    /**
195
     * @inheritDoc
196
     */
197 2
    public function getClass()
198
    {
199 2
        $parameterType = $this->parameterNode->type;
200 2
        if ($parameterType instanceof Name) {
201 2
            if (!$parameterType instanceof Name\FullyQualified) {
202 1
                $parameterTypeName = $parameterType->toString();
203
204 1
                if ('self' === $parameterTypeName) {
205 1
                    return $this->getDeclaringClass();
206
                }
207
208
                // The PHP documentation here:
209
                //     http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration
210
                // seems to indicate that 'parent' is *NOT* a valid typehint.
211
                // Testing PHP itself confirms that it *IS* valid.
212 1
                if ('parent' === $parameterTypeName) {
213 1
                    return $this->getDeclaringClass()->getParentClass();
214
                }
215
216
                throw new ReflectionException("Can not resolve a class name for parameter");
217
            }
218 1
            $className   = $parameterType->toString();
219
220 1
            return new ReflectionClass($className);
221
        }
222
223 1
        return null;
224
    }
225
226
    /**
227
     * {@inheritDoc}
228
     * @return ReflectionClass|null  The class of the method that declared the
229
     *                                   parameter, if any.
230
     */
231 3
    public function getDeclaringClass()
232
    {
233 3
        if ($this->declaringFunction instanceof \ReflectionMethod) {
234 2
            return $this->declaringFunction->getDeclaringClass();
235
        };
236
237 1
        return null;
238
    }
239
240
    /**
241
     * {@inheritDoc}
242
     * @return ReflectionFunction  The function that declared the parameter.
243
     */
244 61
    public function getDeclaringFunction()
245
    {
246 61
        return $this->declaringFunction;
247
    }
248
249
    /**
250
     * {@inheritDoc}
251
     */
252 28
    public function getDefaultValue()
253
    {
254 28
        if (!$this->isDefaultValueAvailable()) {
255 1
            throw new ReflectionException('Internal error: Failed to retrieve the default value');
256
        }
257
258 27
        return $this->defaultValue;
259
    }
260
261
    /**
262
     * {@inheritDoc}
263
     */
264 12
    public function getDefaultValueConstantName()
265
    {
266 12
        if (!$this->isDefaultValueAvailable()) {
267 1
            throw new ReflectionException('Internal error: Failed to retrieve the default value');
268
        }
269
270 11
        return $this->defaultValueConstantName;
271
    }
272
273
    /**
274
     * @inheritDoc
275
     */
276 71
    public function getName()
277
    {
278 71
        return $this->parameterNode->name;
279
    }
280
281
    /**
282
     * {@inheritDoc}
283
     */
284 18
    public function getPosition()
285
    {
286 18
        return $this->parameterIndex;
287
    }
288
289
    /**
290
     * @inheritDoc
291
     */
292 71
    public function getType()
293
    {
294 71
        $isBuiltin     = false;
295 71
        $parameterType = $this->parameterNode->type;
296 71
        if ($parameterType instanceof NullableType) {
297 12
            $parameterType = $parameterType->type;
298
        }
299
300 71
        $allowsNull = $this->allowsNull();
301 71
        if (is_object($parameterType)) {
302 9
            $parameterType = $parameterType->toString();
303 69
        } elseif (is_string($parameterType)) {
304 45
            $isBuiltin = true;
305
        } else {
306 28
            return null;
307
        }
308
309 47
        return new ReflectionType($parameterType, $allowsNull, $isBuiltin);
310
    }
311
312
    /**
313
     * @inheritDoc
314
     */
315 19
    public function hasType()
316
    {
317 19
        $hasType = isset($this->parameterNode->type);
318
319 19
        return $hasType;
320
    }
321
322
    /**
323
     * @inheritDoc
324
     */
325 18
    public function isArray()
326
    {
327 18
        return 'array' === $this->parameterNode->type;
328
    }
329
330
    /**
331
     * @inheritDoc
332
     */
333 18
    public function isCallable()
334
    {
335 18
        return 'callable' === $this->parameterNode->type;
336
    }
337
338
    /**
339
     * @inheritDoc
340
     */
341 76
    public function isDefaultValueAvailable()
342
    {
343
        return
344 76
            isset($this->parameterNode->default) &&
345 76
            !($this->parameterNode->default->getAttribute('implied', false));
346
    }
347
348
    /**
349
     * {@inheritDoc}
350
     */
351 11
    public function isDefaultValueConstant()
352
    {
353 11
        return $this->isDefaultValueConstant;
354
    }
355
356
    /**
357
     * {@inheritDoc}
358
     */
359 145
    public function isOptional()
360
    {
361 145
        return $this->isVariadic() || ($this->isDefaultValueSet() && $this->haveSiblingsDefalutValues());
362
    }
363
364
    /**
365
     * @inheritDoc
366
     */
367 70
    public function isPassedByReference()
368
    {
369 70
        return (bool) $this->parameterNode->byRef;
370
    }
371
372
    /**
373
     * @inheritDoc
374
     */
375 203
    public function isVariadic()
376
    {
377 203
        return (bool) $this->parameterNode->variadic;
378
    }
379
380
    /**
381
     * Returns if default value set (or implied).
382
     *
383
     * Identical to isDefaultValueAvailable(), except it
384
     * includes IMPLIED default values which:
385
     *     + Only exist in builtin functions and methods.
386
     *     + Only affect the optionality of a prameter, not
387
     *         if a SUPPLIED parameter can have the default
388
     *         value.
389
     *
390
     * @return bool
391
     */
392 152
    protected function isDefaultValueSet()
393
    {
394 152
        return isset($this->parameterNode->default);
395
    }
396
397
    /**
398
     * Returns if all following parameters have a default value definition.
399
     *
400
     * @return bool
401
     * @throws ReflectionException If could not fetch declaring function reflection
402
     */
403 61
    protected function haveSiblingsDefalutValues()
404
    {
405 61
        $function = $this->getDeclaringFunction();
406 61
        if (null === $function) {
407
            throw new ReflectionException('Could not get the declaring function reflection.');
408
        }
409
410
        /** @var \ReflectionParameter[] $remainingParameters */
411 61
        $remainingParameters = array_slice($function->getParameters(), $this->parameterIndex + 1);
412 61
        foreach ($remainingParameters as $reflectionParameter) {
413 38
            if (!$reflectionParameter->isDefaultValueSet()) {
0 ignored issues
show
Bug introduced by Loren Osborn
It seems like you code against a specific sub-type and not the parent class ReflectionParameter as the method isDefaultValueSet() does only exist in the following sub-classes of ReflectionParameter: Go\ParserReflection\ReflectionParameter. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
414 38
                return false;
415
            }
416
        }
417
418 57
        return true;
419
    }
420
421
    /**
422
     * Has class been loaded by PHP.
423
     *
424
     * @return bool
425
     *     If class file was included.
426
     */
427
    public function wasIncluded()
428
    {
429
        $hintedClass = $this->getClass();
430
        return
431
            $this->getDeclaringFunction()->wasIncluded() &&
432
            (!$hintedClass || $hintedClass->wasIncluded());
433
    }
434
}
435