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
     * @deprecated since 2.0.13. Use [[ExitCode::OK]] instead.
43
     */
44
    const EXIT_CODE_NORMAL = 0;
45
    /**
46
     * @deprecated since 2.0.13. Use [[ExitCode::UNSPECIFIED_ERROR]] instead.
47
     */
48
    const EXIT_CODE_ERROR = 1;
49
50
    /**
51
     * @var bool whether to run the command interactively.
52
     */
53
    public $interactive = true;
54
    /**
55
     * @var bool|null whether to enable ANSI color in the output.
56
     * If not set, ANSI color will only be enabled for terminals that support it.
57
     */
58
    public $color;
59
    /**
60
     * @var bool whether to display help information about current command.
61
     * @since 2.0.10
62
     */
63
    public $help = false;
64
    /**
65
     * @var bool|null if true - script finish with `ExitCode::OK` in case of exception.
66
     * false - `ExitCode::UNSPECIFIED_ERROR`.
67
     * Default: `YII_ENV_TEST`
68
     * @since 2.0.36
69
     */
70
    public $silentExitOnException;
71
72
    /**
73
     * @var array the options passed during execution.
74
     */
75
    private $_passedOptions = [];
76
77
78
    /**
79
     * {@inheritdoc}
80
     */
81 222
    public function beforeAction($action)
82
    {
83 222
        $silentExit = $this->silentExitOnException !== null ? $this->silentExitOnException : YII_ENV_TEST;
84 222
        Yii::$app->errorHandler->silentExitOnException = $silentExit;
85
86 222
        return parent::beforeAction($action);
87
    }
88
89
    /**
90
     * Returns a value indicating whether ANSI color is enabled.
91
     *
92
     * ANSI color is enabled only if [[color]] is set true or is not set
93
     * and the terminal supports ANSI color.
94
     *
95
     * @param resource $stream the stream to check.
96
     * @return bool Whether to enable ANSI style in output.
97
     */
98 7
    public function isColorEnabled($stream = \STDOUT)
99
    {
100 7
        return $this->color === null ? Console::streamSupportsAnsiColors($stream) : $this->color;
101
    }
102
103
    /**
104
     * Runs an action with the specified action ID and parameters.
105
     * If the action ID is empty, the method will use [[defaultAction]].
106
     * @param string $id the ID of the action to be executed.
107
     * @param array $params the parameters (name-value pairs) to be passed to the action.
108
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
109
     * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully.
110
     * @throws Exception if there are unknown options or missing arguments
111
     * @see createAction
112
     */
113 222
    public function runAction($id, $params = [])
114
    {
115 222
        if (!empty($params)) {
116
            // populate options here so that they are available in beforeAction().
117 210
            $options = $this->options($id === '' ? $this->defaultAction : $id);
118 210
            if (isset($params['_aliases'])) {
119 1
                $optionAliases = $this->optionAliases();
120 1
                foreach ($params['_aliases'] as $name => $value) {
121 1
                    if (array_key_exists($name, $optionAliases)) {
122 1
                        $params[$optionAliases[$name]] = $value;
123
                    } else {
124
                        $message = Yii::t('yii', 'Unknown alias: -{name}', ['name' => $name]);
125
                        if (!empty($optionAliases)) {
126
                            $aliasesAvailable = [];
127
                            foreach ($optionAliases as $alias => $option) {
128
                                $aliasesAvailable[] = '-' . $alias . ' (--' . $option . ')';
129
                            }
130
131
                            $message .= '. ' . Yii::t('yii', 'Aliases available: {aliases}', [
132
                                'aliases' => implode(', ', $aliasesAvailable)
133
                            ]);
134
                        }
135
                        throw new Exception($message);
136
                    }
137
                }
138 1
                unset($params['_aliases']);
139
            }
140 210
            foreach ($params as $name => $value) {
141
                // Allow camelCase options to be entered in kebab-case
142 210
                if (!in_array($name, $options, true) && strpos($name, '-') !== false) {
143 1
                    $kebabName = $name;
144 1
                    $altName = lcfirst(Inflector::id2camel($kebabName));
145 1
                    if (in_array($altName, $options, true)) {
146 1
                        $name = $altName;
147
                    }
148
                }
149
150 210
                if (in_array($name, $options, true)) {
151 54
                    $default = $this->$name;
152 54
                    if (is_array($default) && is_string($value)) {
153 53
                        $this->$name = preg_split('/\s*,\s*(?![^()]*\))/', $value);
154 12
                    } elseif ($default !== null) {
155 11
                        settype($value, gettype($default));
156 11
                        $this->$name = $value;
157
                    } else {
158 2
                        $this->$name = $value;
159
                    }
160 54
                    $this->_passedOptions[] = $name;
161 54
                    unset($params[$name]);
162 54
                    if (isset($kebabName)) {
163 54
                        unset($params[$kebabName]);
164
                    }
165 203
                } elseif (!is_int($name)) {
166
                    $message = Yii::t('yii', 'Unknown option: --{name}', ['name' => $name]);
167
                    if (!empty($options)) {
168
                        $message .= '. ' . Yii::t('yii', 'Options available: {options}', ['options' => '--' . implode(', --', $options)]);
169
                    }
170
171
                    throw new Exception($message);
172
                }
173
            }
174
        }
175 222
        if ($this->help) {
176 2
            $route = $this->getUniqueId() . '/' . $id;
177 2
            return Yii::$app->runAction('help', [$route]);
178
        }
179
180 222
        return parent::runAction($id, $params);
181
    }
182
183
    /**
184
     * Binds the parameters to the action.
185
     * This method is invoked by [[Action]] when it begins to run with the given parameters.
186
     * This method will first bind the parameters with the [[options()|options]]
187
     * available to the action. It then validates the given arguments.
188
     * @param Action $action the action to be bound with parameters
189
     * @param array $params the parameters to be bound to the action
190
     * @return array the valid parameters that the action can run with.
191
     * @throws Exception if there are unknown options or missing arguments
192
     */
193 236
    public function bindActionParams($action, $params)
194
    {
195 236
        if ($action instanceof InlineAction) {
196 236
            $method = new \ReflectionMethod($this, $action->actionMethod);
197
        } else {
198
            $method = new \ReflectionMethod($action, 'run');
199
        }
200
201 236
        $paramKeys = array_keys($params);
202 236
        $args = [];
203 236
        $missing = [];
204 236
        $actionParams = [];
205 236
        $requestedParams = [];
206 236
        foreach ($method->getParameters() as $i => $param) {
207 229
            $name = $param->getName();
208 229
            $key = null;
209 229
            if (array_key_exists($i, $params)) {
210 203
                $key = $i;
211 63
            } elseif (array_key_exists($name, $params)) {
212 7
                $key = $name;
213
            }
214
215 229
            if ($key !== null) {
216 210
                if ($param->isVariadic()) {
217 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...
218 1
                        $jKey = $paramKeys[$j];
219 1
                        if ($jKey !== $key && !is_int($jKey)) {
220
                            break;
221
                        }
222 1
                        $args[] = $actionParams[$key][] = $params[$jKey];
223 1
                        unset($params[$jKey]);
224
                    }
225
                } else {
226 210
                    if (PHP_VERSION_ID >= 80000) {
227 210
                        $isArray = ($type = $param->getType()) instanceof \ReflectionNamedType && $type->getName() === 'array';
228
                    } else {
229
                        $isArray = $param->isArray();
230
                    }
231 210
                    if ($isArray) {
232 1
                        $params[$key] = $params[$key] === '' ? [] : preg_split('/\s*,\s*/', $params[$key]);
233
                    }
234 210
                    $args[] = $actionParams[$key] = $params[$key];
235 210
                    unset($params[$key]);
236
                }
237
            } elseif (
238 59
                PHP_VERSION_ID >= 70100
239 59
                && ($type = $param->getType()) !== null
240 59
                && $type instanceof \ReflectionNamedType
241 59
                && !$type->isBuiltin()
242
            ) {
243
                try {
244 5
                    $this->bindInjectedParams($type, $name, $args, $requestedParams);
245 2
                } catch (\yii\base\Exception $e) {
246 5
                    throw new Exception($e->getMessage());
247
                }
248 54
            } elseif ($param->isDefaultValueAvailable()) {
249 54
                $args[] = $actionParams[$i] = $param->getDefaultValue();
250
            } else {
251 1
                $missing[] = $name;
252
            }
253
        }
254
255 234
        if (!empty($missing)) {
256 1
            throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', $missing)]));
257
        }
258
259
        // We use a different array here, specifically one that doesn't contain service instances but descriptions instead.
260 234
        if (\Yii::$app->requestedParams === null) {
261 234
            \Yii::$app->requestedParams = array_merge($actionParams, $requestedParams);
262
        }
263
264 234
        return array_merge($args, $params);
265
    }
266
267
    /**
268
     * Formats a string with ANSI codes.
269
     *
270
     * You may pass additional parameters using the constants defined in [[\yii\helpers\Console]].
271
     *
272
     * Example:
273
     *
274
     * ```
275
     * echo $this->ansiFormat('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
276
     * ```
277
     *
278
     * @param string $string the string to be formatted
279
     * @return string
280
     */
281 7
    public function ansiFormat($string)
282
    {
283 7
        if ($this->isColorEnabled()) {
284 7
            $args = func_get_args();
285 7
            array_shift($args);
286 7
            $string = Console::ansiFormat($string, $args);
287
        }
288
289 7
        return $string;
290
    }
291
292
    /**
293
     * Prints a string to STDOUT.
294
     *
295
     * You may optionally format the string with ANSI codes by
296
     * passing additional parameters using the constants defined in [[\yii\helpers\Console]].
297
     *
298
     * Example:
299
     *
300
     * ```
301
     * $this->stdout('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
302
     * ```
303
     *
304
     * @param string $string the string to print
305
     * @param int ...$args additional parameters to decorate the output
306
     * @return int|bool Number of bytes printed or false on error
307
     */
308
    public function stdout($string)
309
    {
310
        if ($this->isColorEnabled()) {
311
            $args = func_get_args();
312
            array_shift($args);
313
            $string = Console::ansiFormat($string, $args);
314
        }
315
316
        return Console::stdout($string);
317
    }
318
319
    /**
320
     * Prints a string to STDERR.
321
     *
322
     * You may optionally format the string with ANSI codes by
323
     * passing additional parameters using the constants defined in [[\yii\helpers\Console]].
324
     *
325
     * Example:
326
     *
327
     * ```
328
     * $this->stderr('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
329
     * ```
330
     *
331
     * @param string $string the string to print
332
     * @param int ...$args additional parameters to decorate the output
333
     * @return int|bool Number of bytes printed or false on error
334
     */
335
    public function stderr($string)
336
    {
337
        if ($this->isColorEnabled(\STDERR)) {
338
            $args = func_get_args();
339
            array_shift($args);
340
            $string = Console::ansiFormat($string, $args);
341
        }
342
343
        return fwrite(\STDERR, $string);
344
    }
345
346
    /**
347
     * Prompts the user for input and validates it.
348
     *
349
     * @param string $text prompt string
350
     * @param array $options the options to validate the input:
351
     *
352
     *  - required: whether it is required or not
353
     *  - default: default value if no input is inserted by the user
354
     *  - pattern: regular expression pattern to validate user input
355
     *  - validator: a callable function to validate input. The function must accept two parameters:
356
     *      - $input: the user input to validate
357
     *      - $error: the error value passed by reference if validation failed.
358
     *
359
     * An example of how to use the prompt method with a validator function.
360
     *
361
     * ```php
362
     * $code = $this->prompt('Enter 4-Chars-Pin', ['required' => true, 'validator' => function($input, &$error) {
363
     *     if (strlen($input) !== 4) {
364
     *         $error = 'The Pin must be exactly 4 chars!';
365
     *         return false;
366
     *     }
367
     *     return true;
368
     * }]);
369
     * ```
370
     *
371
     * @return string the user input
372
     */
373
    public function prompt($text, $options = [])
374
    {
375
        if ($this->interactive) {
376
            return Console::prompt($text, $options);
377
        }
378
379
        return isset($options['default']) ? $options['default'] : '';
380
    }
381
382
    /**
383
     * Asks user to confirm by typing y or n.
384
     *
385
     * A typical usage looks like the following:
386
     *
387
     * ```php
388
     * if ($this->confirm("Are you sure?")) {
389
     *     echo "user typed yes\n";
390
     * } else {
391
     *     echo "user typed no\n";
392
     * }
393
     * ```
394
     *
395
     * @param string $message to echo out before waiting for user input
396
     * @param bool $default this value is returned if no selection is made.
397
     * @return bool whether user confirmed.
398
     * Will return true if [[interactive]] is false.
399
     */
400 153
    public function confirm($message, $default = false)
401
    {
402 153
        if ($this->interactive) {
403
            return Console::confirm($message, $default);
404
        }
405
406 153
        return true;
407
    }
408
409
    /**
410
     * Gives the user an option to choose from. Giving '?' as an input will show
411
     * a list of options to choose from and their explanations.
412
     *
413
     * @param string $prompt the prompt message
414
     * @param array $options Key-value array of options to choose from
415
     * @param string|null $default value to use when the user doesn't provide an option.
416
     * If the default is `null`, the user is required to select an option.
417
     *
418
     * @return string An option character the user chose
419
     * @since 2.0.49 Added the $default argument
420
     */
421
    public function select($prompt, $options = [], $default = null)
422
    {
423
        if ($this->interactive) {
424
            return Console::select($prompt, $options, $default);
425
        }
426
427
        return $default;
428
    }
429
430
    /**
431
     * Returns the names of valid options for the action (id)
432
     * An option requires the existence of a public member variable whose
433
     * name is the option name.
434
     * Child classes may override this method to specify possible options.
435
     *
436
     * Note that the values setting via options are not available
437
     * until [[beforeAction()]] is being called.
438
     *
439
     * @param string $actionID the action id of the current request
440
     * @return string[] the names of the options valid for the action
441
     */
442 213
    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

442
    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...
443
    {
444
        // $actionId might be used in subclasses to provide options specific to action id
445 213
        return ['color', 'interactive', 'help', 'silentExitOnException'];
446
    }
447
448
    /**
449
     * Returns option alias names.
450
     * Child classes may override this method to specify alias options.
451
     *
452
     * @return array the options alias names valid for the action
453
     * where the keys is alias name for option and value is option name.
454
     *
455
     * @since 2.0.8
456
     * @see options()
457
     */
458 2
    public function optionAliases()
459
    {
460 2
        return [
461 2
            'h' => 'help',
462 2
        ];
463
    }
464
465
    /**
466
     * Returns properties corresponding to the options for the action id
467
     * Child classes may override this method to specify possible properties.
468
     *
469
     * @param string $actionID the action id of the current request
470
     * @return array properties corresponding to the options for the action
471
     */
472 66
    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

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