Completed
Push — remove-intl-polyfills ( 129df4...a6e727 )
by Alexander
16:22 queued 12:49
created

Controller::select()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 0
cts 2
cp 0
cc 1
eloc 2
nc 1
nop 2
crap 2
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\console;
9
10
use Yii;
11
use yii\base\Action;
12
use yii\base\InlineAction;
13
use yii\base\InvalidRouteException;
14
use yii\helpers\Console;
15
use yii\helpers\Inflector;
16
17
/**
18
 * Controller is the base class of console command classes.
19
 *
20
 * A console controller consists of one or several actions known as sub-commands.
21
 * Users call a console command by specifying the corresponding route which identifies a controller action.
22
 * The `yii` program is used when calling a console command, like the following:
23
 *
24
 * ```
25
 * yii <route> [--param1=value1 --param2 ...]
26
 * ```
27
 *
28
 * where `<route>` is a route to a controller action and the params will be populated as properties of a command.
29
 * See [[options()]] for details.
30
 *
31
 * @property string $help This property is read-only.
32
 * @property string $helpSummary This property is read-only.
33
 * @property array $passedOptionValues The properties corresponding to the passed options. This property is
34
 * read-only.
35
 * @property array $passedOptions The names of the options passed during execution. This property is
36
 * read-only.
37
 *
38
 * @author Qiang Xue <[email protected]>
39
 * @since 2.0
40
 */
41
class Controller extends \yii\base\Controller
42
{
43
    /**
44
     * @var bool whether to run the command interactively.
45
     */
46
    public $interactive = true;
47
    /**
48
     * @var bool whether to enable ANSI color in the output.
49
     * If not set, ANSI color will only be enabled for terminals that support it.
50
     */
51
    public $color;
52
    /**
53
     * @var bool whether to display help information about current command.
54
     * @since 2.0.10
55
     */
56
    public $help;
57
58
    /**
59
     * @var array the options passed during execution.
60
     */
61
    private $_passedOptions = [];
62
63
64
    /**
65
     * Returns a value indicating whether ANSI color is enabled.
66
     *
67
     * ANSI color is enabled only if [[color]] is set true or is not set
68
     * and the terminal supports ANSI color.
69
     *
70
     * @param resource $stream the stream to check.
71
     * @return bool Whether to enable ANSI style in output.
72
     */
73 4
    public function isColorEnabled($stream = \STDOUT)
74
    {
75 4
        return $this->color === null ? Console::streamSupportsAnsiColors($stream) : $this->color;
76
    }
77
78
    /**
79
     * Runs an action with the specified action ID and parameters.
80
     * If the action ID is empty, the method will use [[defaultAction]].
81
     * @param string $id the ID of the action to be executed.
82
     * @param array $params the parameters (name-value pairs) to be passed to the action.
83
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
84
     * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully.
85
     * @throws Exception if there are unknown options or missing arguments
86
     * @see createAction
87
     */
88 112
    public function runAction($id, $params = [])
89
    {
90 112
        if (!empty($params)) {
91
            // populate options here so that they are available in beforeAction().
92 101
            $options = $this->options($id === '' ? $this->defaultAction : $id);
93 101
            if (isset($params['_aliases'])) {
94 1
                $optionAliases = $this->optionAliases();
95 1
                foreach ($params['_aliases'] as $name => $value) {
96 1
                    if (array_key_exists($name, $optionAliases)) {
97 1
                        $params[$optionAliases[$name]] = $value;
98
                    } else {
99 1
                        throw new Exception(Yii::t('yii', 'Unknown alias: -{name}', ['name' => $name]));
100
                    }
101
                }
102 1
                unset($params['_aliases']);
103
            }
104 101
            foreach ($params as $name => $value) {
105
                // Allow camelCase options to be entered in kebab-case
106 101
                if (!in_array($name, $options, true) && strpos($name, '-') !== false) {
107 1
                    $kebabName = $name;
108 1
                    $altName = lcfirst(Inflector::id2camel($kebabName));
109 1
                    if (in_array($altName, $options, true)) {
110 1
                        $name = $altName;
111
                    }
112
                }
113
114 101
                if (in_array($name, $options, true)) {
115 11
                    $default = $this->$name;
116 11
                    if (is_array($default)) {
117 11
                        $this->$name = preg_split('/\s*,\s*(?![^()]*\))/', $value);
118 9
                    } elseif ($default !== null) {
119 8
                        settype($value, gettype($default));
120 8
                        $this->$name = $value;
121
                    } else {
122 1
                        $this->$name = $value;
123
                    }
124 11
                    $this->_passedOptions[] = $name;
125 11
                    unset($params[$name]);
126 11
                    if (isset($kebabName)) {
127 11
                        unset($params[$kebabName]);
128
                    }
129 95
                } elseif (!is_int($name)) {
130 101
                    throw new Exception(Yii::t('yii', 'Unknown option: --{name}', ['name' => $name]));
131
                }
132
            }
133
        }
134 112
        if ($this->help) {
135 2
            $route = $this->getUniqueId() . '/' . $id;
136 2
            return Yii::$app->runAction('help', [$route]);
137
        }
138
139 112
        return parent::runAction($id, $params);
140
    }
141
142
    /**
143
     * Binds the parameters to the action.
144
     * This method is invoked by [[Action]] when it begins to run with the given parameters.
145
     * This method will first bind the parameters with the [[options()|options]]
146
     * available to the action. It then validates the given arguments.
147
     * @param Action $action the action to be bound with parameters
148
     * @param array $params the parameters to be bound to the action
149
     * @return array the valid parameters that the action can run with.
150
     * @throws Exception if there are unknown options or missing arguments
151
     */
152 119
    public function bindActionParams($action, $params)
153
    {
154 119
        if ($action instanceof InlineAction) {
155 119
            $method = new \ReflectionMethod($this, $action->actionMethod);
156
        } else {
157
            $method = new \ReflectionMethod($action, 'run');
158
        }
159
160 119
        $args = array_values($params);
161
162 119
        $missing = [];
163 119
        foreach ($method->getParameters() as $i => $param) {
164 115
            if ($param->isArray() && isset($args[$i])) {
165 1
                $args[$i] = $args[$i] === '' ? [] : preg_split('/\s*,\s*/', $args[$i]);
166
            }
167 115
            if (!isset($args[$i])) {
168 44
                if ($param->isDefaultValueAvailable()) {
169 44
                    $args[$i] = $param->getDefaultValue();
170
                } else {
171 115
                    $missing[] = $param->getName();
0 ignored issues
show
Bug introduced by
Consider using $param->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
172
                }
173
            }
174
        }
175
176 119
        if (!empty($missing)) {
177 1
            throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', $missing)]));
178
        }
179
180 119
        return $args;
181
    }
182
183
    /**
184
     * Formats a string with ANSI codes.
185
     *
186
     * You may pass additional parameters using the constants defined in [[\yii\helpers\Console]].
187
     *
188
     * Example:
189
     *
190
     * ```
191
     * echo $this->ansiFormat('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
192
     * ```
193
     *
194
     * @param string $string the string to be formatted
195
     * @return string
196
     */
197 4
    public function ansiFormat($string)
198
    {
199 4
        if ($this->isColorEnabled()) {
200 4
            $args = func_get_args();
201 4
            array_shift($args);
202 4
            $string = Console::ansiFormat($string, $args);
203
        }
204
205 4
        return $string;
206
    }
207
208
    /**
209
     * Prints a string to STDOUT.
210
     *
211
     * You may optionally format the string with ANSI codes by
212
     * passing additional parameters using the constants defined in [[\yii\helpers\Console]].
213
     *
214
     * Example:
215
     *
216
     * ```
217
     * $this->stdout('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
218
     * ```
219
     *
220
     * @param string $string the string to print
221
     * @return int|bool Number of bytes printed or false on error
222
     */
223
    public function stdout($string)
224
    {
225
        if ($this->isColorEnabled()) {
226
            $args = func_get_args();
227
            array_shift($args);
228
            $string = Console::ansiFormat($string, $args);
229
        }
230
231
        return Console::stdout($string);
232
    }
233
234
    /**
235
     * Prints a string to STDERR.
236
     *
237
     * You may optionally format the string with ANSI codes by
238
     * passing additional parameters using the constants defined in [[\yii\helpers\Console]].
239
     *
240
     * Example:
241
     *
242
     * ```
243
     * $this->stderr('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
244
     * ```
245
     *
246
     * @param string $string the string to print
247
     * @return int|bool Number of bytes printed or false on error
248
     */
249
    public function stderr($string)
250
    {
251
        if ($this->isColorEnabled(\STDERR)) {
252
            $args = func_get_args();
253
            array_shift($args);
254
            $string = Console::ansiFormat($string, $args);
255
        }
256
257
        return fwrite(\STDERR, $string);
258
    }
259
260
    /**
261
     * Prompts the user for input and validates it.
262
     *
263
     * @param string $text prompt string
264
     * @param array $options the options to validate the input:
265
     *
266
     *  - required: whether it is required or not
267
     *  - default: default value if no input is inserted by the user
268
     *  - pattern: regular expression pattern to validate user input
269
     *  - validator: a callable function to validate input. The function must accept two parameters:
270
     *      - $input: the user input to validate
271
     *      - $error: the error value passed by reference if validation failed.
272
     *
273
     * An example of how to use the prompt method with a validator function.
274
     *
275
     * ```php
276
     * $code = $this->prompt('Enter 4-Chars-Pin', ['required' => true, 'validator' => function($input, &$error) {
277
     *     if (strlen($input) !== 4) {
278
     *         $error = 'The Pin must be exactly 4 chars!';
279
     *         return false;
280
     *     }
281
     *     return true;
282
     * }]);
283
     * ```
284
     *
285
     * @return string the user input
286
     */
287
    public function prompt($text, $options = [])
288
    {
289
        if ($this->interactive) {
290
            return Console::prompt($text, $options);
291
        }
292
293
        return $options['default'] ?? '';
294
    }
295
296
    /**
297
     * Asks user to confirm by typing y or n.
298
     *
299
     * A typical usage looks like the following:
300
     *
301
     * ```php
302
     * if ($this->confirm("Are you sure?")) {
303
     *     echo "user typed yes\n";
304
     * } else {
305
     *     echo "user typed no\n";
306
     * }
307
     * ```
308
     *
309
     * @param string $message to echo out before waiting for user input
310
     * @param bool $default this value is returned if no selection is made.
311
     * @return bool whether user confirmed.
312
     * Will return true if [[interactive]] is false.
313
     */
314 67
    public function confirm($message, $default = false)
315
    {
316 67
        if ($this->interactive) {
317
            return Console::confirm($message, $default);
318
        }
319
320 67
        return true;
321
    }
322
323
    /**
324
     * Gives the user an option to choose from. Giving '?' as an input will show
325
     * a list of options to choose from and their explanations.
326
     *
327
     * @param string $prompt the prompt message
328
     * @param array $options Key-value array of options to choose from
329
     *
330
     * @return string An option character the user chose
331
     */
332
    public function select($prompt, $options = [])
333
    {
334
        return Console::select($prompt, $options);
335
    }
336
337
    /**
338
     * Returns the names of valid options for the action (id)
339
     * An option requires the existence of a public member variable whose
340
     * name is the option name.
341
     * Child classes may override this method to specify possible options.
342
     *
343
     * Note that the values setting via options are not available
344
     * until [[beforeAction()]] is being called.
345
     *
346
     * @param string $actionID the action id of the current request
347
     * @return string[] the names of the options valid for the action
348
     */
349 104
    public function options($actionID)
350
    {
351
        // $actionId might be used in subclasses to provide options specific to action id
352 104
        return ['color', 'interactive', 'help'];
353
    }
354
355
    /**
356
     * Returns option alias names.
357
     * Child classes may override this method to specify alias options.
358
     *
359
     * @return array the options alias names valid for the action
360
     * where the keys is alias name for option and value is option name.
361
     *
362
     * @since 2.0.8
363
     * @see options()
364
     */
365 2
    public function optionAliases()
366
    {
367
        return [
368 2
            'h' => 'help',
369
        ];
370
    }
371
372
    /**
373
     * Returns properties corresponding to the options for the action id
374
     * Child classes may override this method to specify possible properties.
375
     *
376
     * @param string $actionID the action id of the current request
377
     * @return array properties corresponding to the options for the action
378
     */
379 42
    public function getOptionValues($actionID)
0 ignored issues
show
Unused Code introduced by
The parameter $actionID is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
380
    {
381
        // $actionId might be used in subclasses to provide properties specific to action id
382 42
        $properties = [];
383 42
        foreach ($this->options($this->action->id) as $property) {
384 42
            $properties[$property] = $this->$property;
385
        }
386
387 42
        return $properties;
388
    }
389
390
    /**
391
     * Returns the names of valid options passed during execution.
392
     *
393
     * @return array the names of the options passed during execution
394
     */
395
    public function getPassedOptions()
396
    {
397
        return $this->_passedOptions;
398
    }
399
400
    /**
401
     * Returns the properties corresponding to the passed options.
402
     *
403
     * @return array the properties corresponding to the passed options
404
     */
405 39
    public function getPassedOptionValues()
406
    {
407 39
        $properties = [];
408 39
        foreach ($this->_passedOptions as $property) {
409
            $properties[$property] = $this->$property;
410
        }
411
412 39
        return $properties;
413
    }
414
415
    /**
416
     * Returns one-line short summary describing this controller.
417
     *
418
     * You may override this method to return customized summary.
419
     * The default implementation returns first line from the PHPDoc comment.
420
     *
421
     * @return string
422
     */
423 3
    public function getHelpSummary()
424
    {
425 3
        return $this->parseDocCommentSummary(new \ReflectionClass($this));
426
    }
427
428
    /**
429
     * Returns help information for this controller.
430
     *
431
     * You may override this method to return customized help.
432
     * The default implementation returns help information retrieved from the PHPDoc comment.
433
     * @return string
434
     */
435
    public function getHelp()
436
    {
437
        return $this->parseDocCommentDetail(new \ReflectionClass($this));
438
    }
439
440
    /**
441
     * Returns a one-line short summary describing the specified action.
442
     * @param Action $action action to get summary for
443
     * @return string a one-line short summary describing the specified action.
444
     */
445 1
    public function getActionHelpSummary($action)
446
    {
447 1
        return $this->parseDocCommentSummary($this->getActionMethodReflection($action));
448
    }
449
450
    /**
451
     * Returns the detailed help information for the specified action.
452
     * @param Action $action action to get help for
453
     * @return string the detailed help information for the specified action.
454
     */
455 2
    public function getActionHelp($action)
456
    {
457 2
        return $this->parseDocCommentDetail($this->getActionMethodReflection($action));
458
    }
459
460
    /**
461
     * Returns the help information for the anonymous arguments for the action.
462
     *
463
     * The returned value should be an array. The keys are the argument names, and the values are
464
     * the corresponding help information. Each value must be an array of the following structure:
465
     *
466
     * - required: boolean, whether this argument is required.
467
     * - type: string, the PHP type of this argument.
468
     * - default: string, the default value of this argument
469
     * - comment: string, the comment of this argument
470
     *
471
     * The default implementation will return the help information extracted from the doc-comment of
472
     * the parameters corresponding to the action method.
473
     *
474
     * @param Action $action
475
     * @return array the help information of the action arguments
476
     */
477 5
    public function getActionArgsHelp($action)
478
    {
479 5
        $method = $this->getActionMethodReflection($action);
480 5
        $tags = $this->parseDocCommentTags($method);
481 5
        $params = isset($tags['param']) ? (array) $tags['param'] : [];
482
483 5
        $args = [];
484
485
        /** @var \ReflectionParameter $reflection */
486 5
        foreach ($method->getParameters() as $i => $reflection) {
487 5
            if ($reflection->getClass() !== null) {
488 1
                continue;
489
            }
490 5
            $name = $reflection->getName();
0 ignored issues
show
Bug introduced by
Consider using $reflection->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
491 5
            $tag = $params[$i] ?? '';
492 5
            if (preg_match('/^(\S+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) {
493 4
                $type = $matches[1];
494 4
                $comment = $matches[3];
495
            } else {
496 1
                $type = null;
497 1
                $comment = $tag;
498
            }
499 5
            if ($reflection->isDefaultValueAvailable()) {
500 2
                $args[$name] = [
501 2
                    'required' => false,
502 2
                    'type' => $type,
503 2
                    'default' => $reflection->getDefaultValue(),
504 2
                    'comment' => $comment,
505
                ];
506
            } else {
507 3
                $args[$name] = [
508 3
                    'required' => true,
509 3
                    'type' => $type,
510
                    'default' => null,
511 5
                    'comment' => $comment,
512
                ];
513
            }
514
        }
515
516 5
        return $args;
517
    }
518
519
    /**
520
     * Returns the help information for the options for the action.
521
     *
522
     * The returned value should be an array. The keys are the option names, and the values are
523
     * the corresponding help information. Each value must be an array of the following structure:
524
     *
525
     * - type: string, the PHP type of this argument.
526
     * - default: string, the default value of this argument
527
     * - comment: string, the comment of this argument
528
     *
529
     * The default implementation will return the help information extracted from the doc-comment of
530
     * the properties corresponding to the action options.
531
     *
532
     * @param Action $action
533
     * @return array the help information of the action options
534
     */
535 3
    public function getActionOptionsHelp($action)
536
    {
537 3
        $optionNames = $this->options($action->id);
538 3
        if (empty($optionNames)) {
539
            return [];
540
        }
541
542 3
        $class = new \ReflectionClass($this);
543 3
        $options = [];
544 3
        foreach ($class->getProperties() as $property) {
545 3
            $name = $property->getName();
546 3
            if (!in_array($name, $optionNames, true)) {
547 3
                continue;
548
            }
549 3
            $defaultValue = $property->getValue($this);
550 3
            $tags = $this->parseDocCommentTags($property);
551
552
            // Display camelCase options in kebab-case
553 3
            $name = Inflector::camel2id($name, '-', true);
554
555 3
            if (isset($tags['var']) || isset($tags['property'])) {
556 3
                $doc = $tags['var'] ?? $tags['property'];
557 3
                if (is_array($doc)) {
558
                    $doc = reset($doc);
559
                }
560 3
                if (preg_match('/^(\S+)(.*)/s', $doc, $matches)) {
561 3
                    $type = $matches[1];
562 3
                    $comment = $matches[2];
563
                } else {
564
                    $type = null;
565
                    $comment = $doc;
566
                }
567 3
                $options[$name] = [
568 3
                    'type' => $type,
569 3
                    'default' => $defaultValue,
570 3
                    'comment' => $comment,
571
                ];
572
            } else {
573
                $options[$name] = [
574
                    'type' => null,
575
                    'default' => $defaultValue,
576 3
                    'comment' => '',
577
                ];
578
            }
579
        }
580
581 3
        return $options;
582
    }
583
584
    private $_reflections = [];
585
586
    /**
587
     * @param Action $action
588
     * @return \ReflectionMethod
589
     */
590 6
    protected function getActionMethodReflection($action)
591
    {
592 6
        if (!isset($this->_reflections[$action->id])) {
593 6
            if ($action instanceof InlineAction) {
594 6
                $this->_reflections[$action->id] = new \ReflectionMethod($this, $action->actionMethod);
595
            } else {
596
                $this->_reflections[$action->id] = new \ReflectionMethod($action, 'run');
597
            }
598
        }
599
600 6
        return $this->_reflections[$action->id];
601
    }
602
603
    /**
604
     * Parses the comment block into tags.
605
     * @param \Reflector $reflection the comment block
606
     * @return array the parsed tags
607
     */
608 5
    protected function parseDocCommentTags($reflection)
609
    {
610 5
        $comment = $reflection->getDocComment();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Reflector as the method getDocComment() does only exist in the following implementations of said interface: ReflectionClass, ReflectionFunction, ReflectionFunctionAbstract, ReflectionMethod, ReflectionObject, ReflectionProperty.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
611 5
        $comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($comment, '/'))), "\r", '');
612 5
        $parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY);
613 5
        $tags = [];
614 5
        foreach ($parts as $part) {
615 5
            if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) {
616 5
                $name = $matches[1];
617 5
                if (!isset($tags[$name])) {
618 5
                    $tags[$name] = trim($matches[2]);
619
                } elseif (is_array($tags[$name])) {
620
                    $tags[$name][] = trim($matches[2]);
621
                } else {
622 5
                    $tags[$name] = [$tags[$name], trim($matches[2])];
623
                }
624
            }
625
        }
626
627 5
        return $tags;
628
    }
629
630
    /**
631
     * Returns the first line of docblock.
632
     *
633
     * @param \Reflector $reflection
634
     * @return string
635
     */
636 3
    protected function parseDocCommentSummary($reflection)
637
    {
638 3
        $docLines = preg_split('~\R~u', $reflection->getDocComment());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Reflector as the method getDocComment() does only exist in the following implementations of said interface: ReflectionClass, ReflectionFunction, ReflectionFunctionAbstract, ReflectionMethod, ReflectionObject, ReflectionProperty.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
639 3
        if (isset($docLines[1])) {
640 3
            return trim($docLines[1], "\t *");
641
        }
642
643 1
        return '';
644
    }
645
646
    /**
647
     * Returns full description from the docblock.
648
     *
649
     * @param \Reflector $reflection
650
     * @return string
651
     */
652 2
    protected function parseDocCommentDetail($reflection)
653
    {
654 2
        $comment = strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($reflection->getDocComment(), '/'))), "\r", '');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Reflector as the method getDocComment() does only exist in the following implementations of said interface: ReflectionClass, ReflectionFunction, ReflectionFunctionAbstract, ReflectionMethod, ReflectionObject, ReflectionProperty.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
655 2
        if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) {
656 2
            $comment = trim(substr($comment, 0, $matches[0][1]));
657
        }
658 2
        if ($comment !== '') {
659 2
            return rtrim(Console::renderColoredString(Console::markdownToAnsi($comment)));
660
        }
661
662
        return '';
663
    }
664
}
665