Controller::bindActionParams()   F
last analyzed

Complexity

Conditions 22
Paths 264

Size

Total Lines 72
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 22.1341

Importance

Changes 0
Metric Value
cc 22
eloc 51
nc 264
nop 2
dl 0
loc 72
ccs 43
cts 46
cp 0.9348
crap 22.1341
rs 2.5333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://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-read string $help The help information for this controller.
32
 * @property-read string $helpSummary The one-line short summary describing this controller.
33
 * @property-read array $passedOptionValues The properties corresponding to the passed options.
34
 * @property-read array $passedOptions The names of the options passed during execution.
35
 *
36
 * @author Qiang Xue <[email protected]>
37
 * @since 2.0
38
 */
39
class Controller extends \yii\base\Controller
40
{
41
    /**
42
     * @var bool whether to run the command interactively.
43
     */
44
    public $interactive = true;
45
    /**
46
     * @var bool|null whether to enable ANSI color in the output.
47
     * If not set, ANSI color will only be enabled for terminals that support it.
48
     */
49
    public $color;
50
    /**
51
     * @var bool whether to display help information about current command.
52
     * @since 2.0.10
53
     */
54
    public $help = false;
55
    /**
56
     * @var bool|null if true - script finish with `ExitCode::OK` in case of exception.
57
     * false - `ExitCode::UNSPECIFIED_ERROR`.
58
     * Default: `YII_ENV_TEST`
59
     * @since 2.0.36
60
     */
61
    public $silentExitOnException;
62
63
    /**
64
     * @var array the options passed during execution.
65
     */
66
    private $_passedOptions = [];
67
68
69
    /**
70
     * {@inheritdoc}
71
     */
72 63
    public function beforeAction($action)
73
    {
74 63
        $silentExit = $this->silentExitOnException !== null ? $this->silentExitOnException : YII_ENV_TEST;
75 63
        Yii::$app->errorHandler->silentExitOnException = $silentExit;
76
77 63
        return parent::beforeAction($action);
78
    }
79
80
    /**
81
     * Returns a value indicating whether ANSI color is enabled.
82
     *
83
     * ANSI color is enabled only if [[color]] is set true or is not set
84
     * and the terminal supports ANSI color.
85
     *
86
     * @param resource $stream the stream to check.
87
     * @return bool Whether to enable ANSI style in output.
88
     */
89 7
    public function isColorEnabled($stream = \STDOUT)
90
    {
91 7
        return $this->color === null ? Console::streamSupportsAnsiColors($stream) : $this->color;
92
    }
93
94
    /**
95
     * Runs an action with the specified action ID and parameters.
96
     * If the action ID is empty, the method will use [[defaultAction]].
97
     * @param string $id the ID of the action to be executed.
98
     * @param array $params the parameters (name-value pairs) to be passed to the action.
99
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
100
     * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully.
101
     * @throws Exception if there are unknown options or missing arguments
102
     * @see createAction
103
     */
104 63
    public function runAction($id, $params = [])
105
    {
106 63
        if (!empty($params)) {
107
            // populate options here so that they are available in beforeAction().
108 62
            $options = $this->options($id === '' ? $this->defaultAction : $id);
109 62
            if (isset($params['_aliases'])) {
110 1
                $optionAliases = $this->optionAliases();
111 1
                foreach ($params['_aliases'] as $name => $value) {
112 1
                    if (array_key_exists($name, $optionAliases)) {
113 1
                        $params[$optionAliases[$name]] = $value;
114
                    } else {
115
                        $message = Yii::t('yii', 'Unknown alias: -{name}', ['name' => $name]);
116
                        if (!empty($optionAliases)) {
117
                            $aliasesAvailable = [];
118
                            foreach ($optionAliases as $alias => $option) {
119
                                $aliasesAvailable[] = '-' . $alias . ' (--' . $option . ')';
120
                            }
121
122
                            $message .= '. ' . Yii::t('yii', 'Aliases available: {aliases}', [
123
                                'aliases' => implode(', ', $aliasesAvailable)
124
                            ]);
125
                        }
126
                        throw new Exception($message);
127
                    }
128
                }
129 1
                unset($params['_aliases']);
130
            }
131 62
            foreach ($params as $name => $value) {
132
                // Allow camelCase options to be entered in kebab-case
133 62
                if (!in_array($name, $options, true) && strpos($name, '-') !== false) {
134 1
                    $kebabName = $name;
135 1
                    $altName = lcfirst(Inflector::id2camel($kebabName));
136 1
                    if (in_array($altName, $options, true)) {
137 1
                        $name = $altName;
138
                    }
139
                }
140
141 62
                if (in_array($name, $options, true)) {
142 2
                    $default = $this->$name;
143 2
                    if (is_array($default) && is_string($value)) {
144 1
                        $this->$name = preg_split('/\s*,\s*(?![^()]*\))/', $value);
145 2
                    } elseif ($default !== null) {
146 1
                        settype($value, gettype($default));
147 1
                        $this->$name = $value;
148
                    } else {
149 2
                        $this->$name = $value;
150
                    }
151 2
                    $this->_passedOptions[] = $name;
152 2
                    unset($params[$name]);
153 2
                    if (isset($kebabName)) {
154 2
                        unset($params[$kebabName]);
155
                    }
156 61
                } elseif (!is_int($name)) {
157
                    $message = Yii::t('yii', 'Unknown option: --{name}', ['name' => $name]);
158
                    if (!empty($options)) {
159
                        $message .= '. ' . Yii::t('yii', 'Options available: {options}', ['options' => '--' . implode(', --', $options)]);
160
                    }
161
162
                    throw new Exception($message);
163
                }
164
            }
165
        }
166 63
        if ($this->help) {
167 2
            $route = $this->getUniqueId() . '/' . $id;
168 2
            return Yii::$app->runAction('help', [$route]);
169
        }
170
171 63
        return parent::runAction($id, $params);
172
    }
173
174
    /**
175
     * Binds the parameters to the action.
176
     * This method is invoked by [[Action]] when it begins to run with the given parameters.
177
     * This method will first bind the parameters with the [[options()|options]]
178
     * available to the action. It then validates the given arguments.
179
     * @param Action $action the action to be bound with parameters
180
     * @param array $params the parameters to be bound to the action
181
     * @return array the valid parameters that the action can run with.
182
     * @throws Exception if there are unknown options or missing arguments
183
     */
184 77
    public function bindActionParams($action, $params)
185
    {
186 77
        if ($action instanceof InlineAction) {
187 77
            $method = new \ReflectionMethod($this, $action->actionMethod);
188
        } else {
189
            $method = new \ReflectionMethod($action, 'run');
190
        }
191
192 77
        $paramKeys = array_keys($params);
193 77
        $args = [];
194 77
        $missing = [];
195 77
        $actionParams = [];
196 77
        $requestedParams = [];
197 77
        foreach ($method->getParameters() as $i => $param) {
198 72
            $name = $param->getName();
199 72
            $key = null;
200 72
            if (array_key_exists($i, $params)) {
201 61
                $key = $i;
202 13
            } elseif (array_key_exists($name, $params)) {
203 7
                $key = $name;
204
            }
205
206 72
            if ($key !== null) {
207 68
                if ($param->isVariadic()) {
208 1
                    for ($j = array_search($key, $paramKeys); $j < count($paramKeys); $j++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
209 1
                        $jKey = $paramKeys[$j];
210 1
                        if ($jKey !== $key && !is_int($jKey)) {
211
                            break;
212
                        }
213 1
                        $args[] = $actionParams[$key][] = $params[$jKey];
214 1
                        unset($params[$jKey]);
215
                    }
216
                } else {
217 68
                    if (PHP_VERSION_ID >= 80000) {
218 68
                        $isArray = ($type = $param->getType()) instanceof \ReflectionNamedType && $type->getName() === 'array';
219
                    } else {
220
                        $isArray = $param->isArray();
221
                    }
222 68
                    if ($isArray) {
223 1
                        $params[$key] = $params[$key] === '' ? [] : preg_split('/\s*,\s*/', $params[$key]);
224
                    }
225 68
                    $args[] = $actionParams[$key] = $params[$key];
226 68
                    unset($params[$key]);
227
                }
228
            } elseif (
229 9
                PHP_VERSION_ID >= 70100
230 9
                && ($type = $param->getType()) !== null
231 9
                && $type instanceof \ReflectionNamedType
232 9
                && !$type->isBuiltin()
233
            ) {
234
                try {
235 5
                    $this->bindInjectedParams($type, $name, $args, $requestedParams);
236 2
                } catch (\yii\base\Exception $e) {
237 5
                    throw new Exception($e->getMessage());
238
                }
239 4
            } elseif ($param->isDefaultValueAvailable()) {
240 4
                $args[] = $actionParams[$i] = $param->getDefaultValue();
241
            } else {
242 1
                $missing[] = $name;
243
            }
244
        }
245
246 75
        if (!empty($missing)) {
247 1
            throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', $missing)]));
248
        }
249
250
        // We use a different array here, specifically one that doesn't contain service instances but descriptions instead.
251 75
        if (\Yii::$app->requestedParams === null) {
252 75
            \Yii::$app->requestedParams = array_merge($actionParams, $requestedParams);
253
        }
254
255 75
        return array_merge($args, $params);
256
    }
257
258
    /**
259
     * Formats a string with ANSI codes.
260
     *
261
     * You may pass additional parameters using the constants defined in [[\yii\helpers\Console]].
262
     *
263
     * Example:
264
     *
265
     * ```
266
     * echo $this->ansiFormat('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
267
     * ```
268
     *
269
     * @param string $string the string to be formatted
270
     * @return string
271
     */
272 7
    public function ansiFormat($string)
273
    {
274 7
        if ($this->isColorEnabled()) {
275 7
            $args = func_get_args();
276 7
            array_shift($args);
277 7
            $string = Console::ansiFormat($string, $args);
278
        }
279
280 7
        return $string;
281
    }
282
283
    /**
284
     * Prints a string to STDOUT.
285
     *
286
     * You may optionally format the string with ANSI codes by
287
     * passing additional parameters using the constants defined in [[\yii\helpers\Console]].
288
     *
289
     * Example:
290
     *
291
     * ```
292
     * $this->stdout('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
293
     * ```
294
     *
295
     * @param string $string the string to print
296
     * @param int ...$args additional parameters to decorate the output
297
     * @return int|bool Number of bytes printed or false on error
298
     */
299
    public function stdout($string)
300
    {
301
        if ($this->isColorEnabled()) {
302
            $args = func_get_args();
303
            array_shift($args);
304
            $string = Console::ansiFormat($string, $args);
305
        }
306
307
        return Console::stdout($string);
308
    }
309
310
    /**
311
     * Prints a string to STDERR.
312
     *
313
     * You may optionally format the string with ANSI codes by
314
     * passing additional parameters using the constants defined in [[\yii\helpers\Console]].
315
     *
316
     * Example:
317
     *
318
     * ```
319
     * $this->stderr('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
320
     * ```
321
     *
322
     * @param string $string the string to print
323
     * @param int ...$args additional parameters to decorate the output
324
     * @return int|bool Number of bytes printed or false on error
325
     */
326
    public function stderr($string)
327
    {
328
        if ($this->isColorEnabled(\STDERR)) {
329
            $args = func_get_args();
330
            array_shift($args);
331
            $string = Console::ansiFormat($string, $args);
332
        }
333
334
        return fwrite(\STDERR, $string);
335
    }
336
337
    /**
338
     * Prompts the user for input and validates it.
339
     *
340
     * @param string $text prompt string
341
     * @param array $options the options to validate the input:
342
     *
343
     *  - required: whether it is required or not
344
     *  - default: default value if no input is inserted by the user
345
     *  - pattern: regular expression pattern to validate user input
346
     *  - validator: a callable function to validate input. The function must accept two parameters:
347
     *      - $input: the user input to validate
348
     *      - $error: the error value passed by reference if validation failed.
349
     *
350
     * An example of how to use the prompt method with a validator function.
351
     *
352
     * ```php
353
     * $code = $this->prompt('Enter 4-Chars-Pin', ['required' => true, 'validator' => function($input, &$error) {
354
     *     if (strlen($input) !== 4) {
355
     *         $error = 'The Pin must be exactly 4 chars!';
356
     *         return false;
357
     *     }
358
     *     return true;
359
     * }]);
360
     * ```
361
     *
362
     * @return string the user input
363
     */
364
    public function prompt($text, $options = [])
365
    {
366
        if ($this->interactive) {
367
            return Console::prompt($text, $options);
368
        }
369
370
        return isset($options['default']) ? $options['default'] : '';
371
    }
372
373
    /**
374
     * Asks user to confirm by typing y or n.
375
     *
376
     * A typical usage looks like the following:
377
     *
378
     * ```php
379
     * if ($this->confirm("Are you sure?")) {
380
     *     echo "user typed yes\n";
381
     * } else {
382
     *     echo "user typed no\n";
383
     * }
384
     * ```
385
     *
386
     * @param string $message to echo out before waiting for user input
387
     * @param bool $default this value is returned if no selection is made.
388
     * @return bool whether user confirmed.
389
     * Will return true if [[interactive]] is false.
390
     */
391
    public function confirm($message, $default = false)
392
    {
393
        if ($this->interactive) {
394
            return Console::confirm($message, $default);
395
        }
396
397
        return true;
398
    }
399
400
    /**
401
     * Gives the user an option to choose from. Giving '?' as an input will show
402
     * a list of options to choose from and their explanations.
403
     *
404
     * @param string $prompt the prompt message
405
     * @param array $options Key-value array of options to choose from
406
     * @param string|null $default value to use when the user doesn't provide an option.
407
     * If the default is `null`, the user is required to select an option.
408
     *
409
     * @return string An option character the user chose
410
     * @since 2.0.49 Added the $default argument
411
     */
412
    public function select($prompt, $options = [], $default = null)
413
    {
414
        if ($this->interactive) {
415
            return Console::select($prompt, $options, $default);
416
        }
417
418
        return $default;
419
    }
420
421
    /**
422
     * Returns the names of valid options for the action (id)
423
     * An option requires the existence of a public member variable whose
424
     * name is the option name.
425
     * Child classes may override this method to specify possible options.
426
     *
427
     * Note that the values setting via options are not available
428
     * until [[beforeAction()]] is being called.
429
     *
430
     * @param string $actionID the action id of the current request
431
     * @return string[] the names of the options valid for the action
432
     */
433 65
    public function options($actionID)
0 ignored issues
show
Unused Code introduced by
The parameter $actionID is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

433
    public function options(/** @scrutinizer ignore-unused */ $actionID)

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

Loading history...
434
    {
435
        // $actionId might be used in subclasses to provide options specific to action id
436 65
        return ['color', 'interactive', 'help', 'silentExitOnException'];
437
    }
438
439
    /**
440
     * Returns option alias names.
441
     * Child classes may override this method to specify alias options.
442
     *
443
     * @return array the options alias names valid for the action
444
     * where the keys is alias name for option and value is option name.
445
     *
446
     * @since 2.0.8
447
     * @see options()
448
     */
449 2
    public function optionAliases()
450
    {
451 2
        return [
452 2
            'h' => 'help',
453 2
        ];
454
    }
455
456
    /**
457
     * Returns properties corresponding to the options for the action id
458
     * Child classes may override this method to specify possible properties.
459
     *
460
     * @param string $actionID the action id of the current request
461
     * @return array properties corresponding to the options for the action
462
     */
463 49
    public function getOptionValues($actionID)
0 ignored issues
show
Unused Code introduced by
The parameter $actionID is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

463
    public function getOptionValues(/** @scrutinizer ignore-unused */ $actionID)

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

Loading history...
464
    {
465
        // $actionId might be used in subclasses to provide properties specific to action id
466 49
        $properties = [];
467 49
        foreach ($this->options($this->action->id) as $property) {
468 49
            $properties[$property] = $this->$property;
469
        }
470
471 49
        return $properties;
472
    }
473
474
    /**
475
     * Returns the names of valid options passed during execution.
476
     *
477
     * @return array the names of the options passed during execution
478
     */
479
    public function getPassedOptions()
480
    {
481
        return $this->_passedOptions;
482
    }
483
484
    /**
485
     * Returns the properties corresponding to the passed options.
486
     *
487
     * @return array the properties corresponding to the passed options
488
     */
489 45
    public function getPassedOptionValues()
490
    {
491 45
        $properties = [];
492 45
        foreach ($this->_passedOptions as $property) {
493
            $properties[$property] = $this->$property;
494
        }
495
496 45
        return $properties;
497
    }
498
499
    /**
500
     * Returns one-line short summary describing this controller.
501
     *
502
     * You may override this method to return customized summary.
503
     * The default implementation returns first line from the PHPDoc comment.
504
     *
505
     * @return string
506
     */
507 5
    public function getHelpSummary()
508
    {
509 5
        return $this->parseDocCommentSummary(new \ReflectionClass($this));
510
    }
511
512
    /**
513
     * Returns help information for this controller.
514
     *
515
     * You may override this method to return customized help.
516
     * The default implementation returns help information retrieved from the PHPDoc comment.
517
     * @return string
518
     */
519
    public function getHelp()
520
    {
521
        return $this->parseDocCommentDetail(new \ReflectionClass($this));
522
    }
523
524
    /**
525
     * Returns a one-line short summary describing the specified action.
526
     * @param Action $action action to get summary for
527
     * @return string a one-line short summary describing the specified action.
528
     */
529 3
    public function getActionHelpSummary($action)
530
    {
531 3
        if ($action === null) {
532 1
            return $this->ansiFormat(Yii::t('yii', 'Action not found.'), Console::FG_RED);
533
        }
534
535 2
        return $this->parseDocCommentSummary($this->getActionMethodReflection($action));
536
    }
537
538
    /**
539
     * Returns the detailed help information for the specified action.
540
     * @param Action $action action to get help for
541
     * @return string the detailed help information for the specified action.
542
     */
543 3
    public function getActionHelp($action)
544
    {
545 3
        return $this->parseDocCommentDetail($this->getActionMethodReflection($action));
546
    }
547
548
    /**
549
     * Returns the help information for the anonymous arguments for the action.
550
     *
551
     * The returned value should be an array. The keys are the argument names, and the values are
552
     * the corresponding help information. Each value must be an array of the following structure:
553
     *
554
     * - required: bool, whether this argument is required
555
     * - type: string|null, the PHP type(s) of this argument
556
     * - default: mixed, the default value of this argument
557
     * - comment: string, the description of this argument
558
     *
559
     * The default implementation will return the help information extracted from the Reflection or
560
     * DocBlock of the parameters corresponding to the action method.
561
     *
562
     * @param Action $action the action instance
563
     * @return array the help information of the action arguments
564
     */
565 6
    public function getActionArgsHelp($action)
566
    {
567 6
        $method = $this->getActionMethodReflection($action);
568
569 6
        $tags = $this->parseDocCommentTags($method);
570 6
        $tags['param'] = isset($tags['param']) ? (array) $tags['param'] : [];
571 6
        $phpDocParams = [];
572 6
        foreach ($tags['param'] as $i => $tag) {
573 5
            if (preg_match('/^(?<type>\S+)(\s+\$(?<name>\w+))?(?<comment>.*)/us', $tag, $matches) === 1) {
574 5
                $key = empty($matches['name']) ? $i : $matches['name'];
575 5
                $phpDocParams[$key] = ['type' => $matches['type'], 'comment' => $matches['comment']];
576
            }
577
        }
578 6
        unset($tags);
579
580 6
        $args = [];
581
582
        /** @var \ReflectionParameter $parameter */
583 6
        foreach ($method->getParameters() as $i => $parameter) {
584 6
            $type = null;
585 6
            $comment = '';
586 6
            if (PHP_MAJOR_VERSION > 5 && $parameter->hasType()) {
587 1
                $reflectionType = $parameter->getType();
588 1
                if (PHP_VERSION_ID >= 70100) {
589 1
                    $types = method_exists($reflectionType, 'getTypes') ? $reflectionType->getTypes() : [$reflectionType];
590 1
                    foreach ($types as $key => $reflectionType) {
591 1
                        $types[$key] = $reflectionType->getName();
592
                    }
593 1
                    $type = implode('|', $types);
594
                } else {
595
                    $type = (string) $reflectionType;
596
                }
597
            }
598
            // find PhpDoc tag by property name or position
599 6
            $key = isset($phpDocParams[$parameter->name]) ? $parameter->name : (isset($phpDocParams[$i]) ? $i : null);
600 6
            if ($key !== null) {
601 5
                $comment = $phpDocParams[$key]['comment'];
602 5
                if ($type === null && !empty($phpDocParams[$key]['type'])) {
603 5
                    $type = $phpDocParams[$key]['type'];
604
                }
605
            }
606
            // if type still not detected, then using type of default value
607 6
            if ($type === null && $parameter->isDefaultValueAvailable() && $parameter->getDefaultValue() !== null) {
608 1
                $type = gettype($parameter->getDefaultValue());
609
            }
610
611 6
            $args[$parameter->name] = [
612 6
                'required' => !$parameter->isOptional(),
613 6
                'type' => $type,
614 6
                'default' => $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null,
615 6
                'comment' => $comment,
616 6
            ];
617
        }
618
619 6
        return $args;
620
    }
621
622
    /**
623
     * Returns the help information for the options for the action.
624
     *
625
     * The returned value should be an array. The keys are the option names, and the values are
626
     * the corresponding help information. Each value must be an array of the following structure:
627
     *
628
     * - type: string, the PHP type of this argument.
629
     * - default: string, the default value of this argument
630
     * - comment: string, the comment of this argument
631
     *
632
     * The default implementation will return the help information extracted from the doc-comment of
633
     * the properties corresponding to the action options.
634
     *
635
     * @param Action $action
636
     * @return array the help information of the action options
637
     */
638 4
    public function getActionOptionsHelp($action)
639
    {
640 4
        $optionNames = $this->options($action->id);
641 4
        if (empty($optionNames)) {
642
            return [];
643
        }
644
645 4
        $class = new \ReflectionClass($this);
646 4
        $options = [];
647 4
        foreach ($class->getProperties() as $property) {
648 4
            $name = $property->getName();
649 4
            if (!in_array($name, $optionNames, true)) {
650 4
                continue;
651
            }
652 4
            $defaultValue = $property->getValue($this);
653 4
            $tags = $this->parseDocCommentTags($property);
654
655
            // Display camelCase options in kebab-case
656 4
            $name = Inflector::camel2id($name, '-', true);
657
658 4
            if (isset($tags['var']) || isset($tags['property'])) {
659 4
                $doc = isset($tags['var']) ? $tags['var'] : $tags['property'];
660 4
                if (is_array($doc)) {
661
                    $doc = reset($doc);
662
                }
663 4
                if (preg_match('/^(\S+)(.*)/s', $doc, $matches)) {
664 4
                    $type = $matches[1];
665 4
                    $comment = $matches[2];
666
                } else {
667
                    $type = null;
668
                    $comment = $doc;
669
                }
670 4
                $options[$name] = [
671 4
                    'type' => $type,
672 4
                    'default' => $defaultValue,
673 4
                    'comment' => $comment,
674 4
                ];
675
            } else {
676 1
                $options[$name] = [
677 1
                    'type' => null,
678 1
                    'default' => $defaultValue,
679 1
                    'comment' => '',
680 1
                ];
681
            }
682
        }
683
684 4
        return $options;
685
    }
686
687
    private $_reflections = [];
688
689
    /**
690
     * @param Action $action
691
     * @return \ReflectionFunctionAbstract
692
     */
693 8
    protected function getActionMethodReflection($action)
694
    {
695 8
        if (!isset($this->_reflections[$action->id])) {
696 8
            if ($action instanceof InlineAction) {
697 8
                $this->_reflections[$action->id] = new \ReflectionMethod($this, $action->actionMethod);
698
            } else {
699
                $this->_reflections[$action->id] = new \ReflectionMethod($action, 'run');
700
            }
701
        }
702
703 8
        return $this->_reflections[$action->id];
704
    }
705
706
    /**
707
     * Parses the comment block into tags.
708
     * @param \ReflectionClass|\ReflectionProperty|\ReflectionFunctionAbstract $reflection the comment block
709
     * @return array the parsed tags
710
     */
711 6
    protected function parseDocCommentTags($reflection)
712
    {
713 6
        $comment = $reflection->getDocComment();
714 6
        $comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**([ \t])?/m', '', trim($comment, '/'))), "\r", '');
715 6
        $parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY);
716 6
        $tags = [];
717 6
        foreach ($parts as $part) {
718 6
            if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) {
719 6
                $name = $matches[1];
720 6
                if (!isset($tags[$name])) {
721 6
                    $tags[$name] = trim($matches[2]);
722
                } elseif (is_array($tags[$name])) {
723
                    $tags[$name][] = trim($matches[2]);
724
                } else {
725
                    $tags[$name] = [$tags[$name], trim($matches[2])];
726
                }
727
            }
728
        }
729
730 6
        return $tags;
731
    }
732
733
    /**
734
     * Returns the first line of docblock.
735
     *
736
     * @param \ReflectionClass|\ReflectionProperty|\ReflectionFunctionAbstract $reflection
737
     * @return string
738
     */
739 5
    protected function parseDocCommentSummary($reflection)
740
    {
741 5
        $docLines = preg_split('~\R~u', $reflection->getDocComment());
742 5
        if (isset($docLines[1])) {
743 5
            return trim($docLines[1], "\t *");
744
        }
745
746 2
        return '';
747
    }
748
749
    /**
750
     * Returns full description from the docblock.
751
     *
752
     * @param \ReflectionClass|\ReflectionProperty|\ReflectionFunctionAbstract $reflection
753
     * @return string
754
     */
755 3
    protected function parseDocCommentDetail($reflection)
756
    {
757 3
        $comment = strtr(trim(preg_replace('/^\s*\**([ \t])?/m', '', trim($reflection->getDocComment(), '/'))), "\r", '');
758 3
        if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) {
759 2
            $comment = trim(substr($comment, 0, $matches[0][1]));
760
        }
761 3
        if ($comment !== '') {
762 2
            return rtrim(Console::renderColoredString(Console::markdownToAnsi($comment)));
763
        }
764
765 1
        return '';
766
    }
767
}
768