Completed
Push — revert-action-di ( 181365...42b856 )
by Carsten
08:33
created

Controller::getActionArgsHelp()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 37
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 37
ccs 0
cts 28
cp 0
rs 8.439
cc 6
eloc 27
nc 18
nop 1
crap 42
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
16
/**
17
 * Controller is the base class of console command classes.
18
 *
19
 * A console controller consists of one or several actions known as sub-commands.
20
 * Users call a console command by specifying the corresponding route which identifies a controller action.
21
 * The `yii` program is used when calling a console command, like the following:
22
 *
23
 * ```
24
 * yii <route> [--param1=value1 --param2 ...]
25
 * ```
26
 *
27
 * where `<route>` is a route to a controller action and the params will be populated as properties of a command.
28
 * See [[options()]] for details.
29
 *
30
 * @property string $help This property is read-only.
31
 * @property string $helpSummary This property is read-only.
32
 *
33
 * @author Qiang Xue <[email protected]>
34
 * @since 2.0
35
 */
36
class Controller extends \yii\base\Controller
37
{
38
    const EXIT_CODE_NORMAL = 0;
39
    const EXIT_CODE_ERROR = 1;
40
41
    /**
42
     * @var boolean whether to run the command interactively.
43
     */
44
    public $interactive = true;
45
    /**
46
     * @var boolean 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
    /**
52
     * @var array the options passed during execution.
53
     */
54
    private $_passedOptions = [];
55
56
57
    /**
58
     * Returns a value indicating whether ANSI color is enabled.
59
     *
60
     * ANSI color is enabled only if [[color]] is set true or is not set
61
     * and the terminal supports ANSI color.
62
     *
63
     * @param resource $stream the stream to check.
64
     * @return boolean Whether to enable ANSI style in output.
65
     */
66
    public function isColorEnabled($stream = \STDOUT)
67
    {
68
        return $this->color === null ? Console::streamSupportsAnsiColors($stream) : $this->color;
69
    }
70
71
    /**
72
     * Runs an action with the specified action ID and parameters.
73
     * If the action ID is empty, the method will use [[defaultAction]].
74
     * @param string $id the ID of the action to be executed.
75
     * @param array $params the parameters (name-value pairs) to be passed to the action.
76
     * @return integer the status of the action execution. 0 means normal, other values mean abnormal.
77
     * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully.
78
     * @throws Exception if there are unknown options or missing arguments
79
     * @see createAction
80
     */
81 42
    public function runAction($id, $params = [])
82
    {
83 42
        if (!empty($params)) {
84
            // populate options here so that they are available in beforeAction().
85 38
            $options = $this->options($id === '' ? $this->defaultAction : $id);
86 38
            foreach ($params as $name => $value) {
87 38
                if (in_array($name, $options, true)) {
88 4
                    $default = $this->$name;
89 4
                    if (is_array($default)) {
90 4
                        $this->$name = preg_split('/\s*,\s*/', $value);
91 4
                    } elseif ($default !== null) {
92
                        settype($value, gettype($default));
93
                        $this->$name = $value;
94
                    } else {
95
                        $this->$name = $value;
96
                    }
97 4
                    $this->_passedOptions[] = $name;
98 4
                    unset($params[$name]);
99 38
                } elseif (!is_int($name)) {
100
                    throw new Exception(Yii::t('yii', 'Unknown option: --{name}', ['name' => $name]));
101
                }
102 38
            }
103 38
        }
104 42
        return parent::runAction($id, $params);
105
    }
106
107
    /**
108
     * Binds the parameters to the action.
109
     * This method is invoked by [[Action]] when it begins to run with the given parameters.
110
     * This method will first bind the parameters with the [[options()|options]]
111
     * available to the action. It then validates the given arguments.
112
     * @param Action $action the action to be bound with parameters
113
     * @param array $params the parameters to be bound to the action
114
     * @return array the valid parameters that the action can run with.
115
     * @throws Exception if there are unknown options or missing arguments
116
     */
117 42
    public function bindActionParams($action, $params)
118
    {
119 42
        if ($action instanceof InlineAction) {
120 42
            $method = new \ReflectionMethod($this, $action->actionMethod);
121 42
        } else {
122
            $method = new \ReflectionMethod($action, 'run');
123
        }
124
125 42
        $args = array_values($params);
126
127 42
        $missing = [];
128 42
        foreach ($method->getParameters() as $i => $param) {
129 42
            if ($param->isArray() && isset($args[$i])) {
130 1
                $args[$i] = preg_split('/\s*,\s*/', $args[$i]);
131 1
            }
132 42
            if (!isset($args[$i])) {
133 7
                if ($param->isDefaultValueAvailable()) {
134 7
                    $args[$i] = $param->getDefaultValue();
135 7
                } else {
136 1
                    $missing[] = $param->getName();
137
                }
138 7
            }
139 42
        }
140
141 42
        if (!empty($missing)) {
142 1
            throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', $missing)]));
143
        }
144
145 42
        return $args;
146
    }
147
148
    /**
149
     * Formats a string with ANSI codes
150
     *
151
     * You may pass additional parameters using the constants defined in [[\yii\helpers\Console]].
152
     *
153
     * Example:
154
     *
155
     * ```
156
     * echo $this->ansiFormat('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
157
     * ```
158
     *
159
     * @param string $string the string to be formatted
160
     * @return string
161
     */
162
    public function ansiFormat($string)
163
    {
164
        if ($this->isColorEnabled()) {
165
            $args = func_get_args();
166
            array_shift($args);
167
            $string = Console::ansiFormat($string, $args);
168
        }
169
        return $string;
170
    }
171
172
    /**
173
     * Prints a string to STDOUT
174
     *
175
     * You may optionally format the string with ANSI codes by
176
     * passing additional parameters using the constants defined in [[\yii\helpers\Console]].
177
     *
178
     * Example:
179
     *
180
     * ```
181
     * $this->stdout('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
182
     * ```
183
     *
184
     * @param string $string the string to print
185
     * @return int|boolean Number of bytes printed or false on error
186
     */
187
    public function stdout($string)
188
    {
189
        if ($this->isColorEnabled()) {
190
            $args = func_get_args();
191
            array_shift($args);
192
            $string = Console::ansiFormat($string, $args);
193
        }
194
        return Console::stdout($string);
195
    }
196
197
    /**
198
     * Prints a string to STDERR
199
     *
200
     * You may optionally format the string with ANSI codes by
201
     * passing additional parameters using the constants defined in [[\yii\helpers\Console]].
202
     *
203
     * Example:
204
     *
205
     * ```
206
     * $this->stderr('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
207
     * ```
208
     *
209
     * @param string $string the string to print
210
     * @return int|boolean Number of bytes printed or false on error
211
     */
212
    public function stderr($string)
213
    {
214
        if ($this->isColorEnabled(\STDERR)) {
215
            $args = func_get_args();
216
            array_shift($args);
217
            $string = Console::ansiFormat($string, $args);
218
        }
219
        return fwrite(\STDERR, $string);
220
    }
221
222
    /**
223
     * Prompts the user for input and validates it
224
     *
225
     * @param string $text prompt string
226
     * @param array $options the options to validate the input:
227
     *
228
     *  - required: whether it is required or not
229
     *  - default: default value if no input is inserted by the user
230
     *  - pattern: regular expression pattern to validate user input
231
     *  - validator: a callable function to validate input. The function must accept two parameters:
232
     *      - $input: the user input to validate
233
     *      - $error: the error value passed by reference if validation failed.
234
     * @return string the user input
235
     */
236
    public function prompt($text, $options = [])
237
    {
238
        if ($this->interactive) {
239
            return Console::prompt($text, $options);
240
        } else {
241
            return isset($options['default']) ? $options['default'] : '';
242
        }
243
    }
244
245
    /**
246
     * Asks user to confirm by typing y or n.
247
     *
248
     * @param string $message to echo out before waiting for user input
249
     * @param boolean $default this value is returned if no selection is made.
250
     * @return boolean whether user confirmed.
251
     * Will return true if [[interactive]] is false.
252
     */
253 26
    public function confirm($message, $default = false)
254
    {
255 26
        if ($this->interactive) {
256
            return Console::confirm($message, $default);
257
        } else {
258 26
            return true;
259
        }
260
    }
261
262
    /**
263
     * Gives the user an option to choose from. Giving '?' as an input will show
264
     * a list of options to choose from and their explanations.
265
     *
266
     * @param string $prompt the prompt message
267
     * @param array $options Key-value array of options to choose from
268
     *
269
     * @return string An option character the user chose
270
     */
271
    public function select($prompt, $options = [])
272
    {
273
        return Console::select($prompt, $options);
274
    }
275
276
    /**
277
     * Returns the names of valid options for the action (id)
278
     * An option requires the existence of a public member variable whose
279
     * name is the option name.
280
     * Child classes may override this method to specify possible options.
281
     *
282
     * Note that the values setting via options are not available
283
     * until [[beforeAction()]] is being called.
284
     *
285
     * @param string $actionID the action id of the current request
286
     * @return array the names of the options valid for the action
287
     */
288 16
    public function options($actionID)
289
    {
290
        // $actionId might be used in subclasses to provide options specific to action id
291 16
        return ['color', 'interactive'];
292
    }
293
294
    /**
295
     * Returns properties corresponding to the options for the action id
296
     * Child classes may override this method to specify possible properties.
297
     *
298
     * @param string $actionID the action id of the current request
299
     * @return array properties corresponding to the options for the action
300
     */
301 20
    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...
302
    {
303
        // $actionId might be used in subclasses to provide properties specific to action id
304 20
        $properties = [];
305 20
        foreach ($this->options($this->action->id) as $property) {
306 20
            $properties[$property] = $this->$property;
307 20
        }
308 20
        return $properties;
309
    }
310
311
    /**
312
     * Returns the names of valid options passed during execution.
313
     *
314
     * @return array the names of the options passed during execution
315
     */
316
    public function getPassedOptions()
317
    {
318
        return $this->_passedOptions;
319
    }
320
321
    /**
322
     * Returns the properties corresponding to the passed options
323
     *
324
     * @return array the properties corresponding to the passed options
325
     */
326 18
    public function getPassedOptionValues()
327
    {
328 18
        $properties = [];
329 18
        foreach ($this->_passedOptions as $property) {
330
            $properties[$property] = $this->$property;
331 18
        }
332 18
        return $properties;
333
    }
334
335
    /**
336
     * Returns one-line short summary describing this controller.
337
     *
338
     * You may override this method to return customized summary.
339
     * The default implementation returns first line from the PHPDoc comment.
340
     *
341
     * @return string
342
     */
343
    public function getHelpSummary()
344
    {
345
        return $this->parseDocCommentSummary(new \ReflectionClass($this));
346
    }
347
348
    /**
349
     * Returns help information for this controller.
350
     *
351
     * You may override this method to return customized help.
352
     * The default implementation returns help information retrieved from the PHPDoc comment.
353
     * @return string
354
     */
355
    public function getHelp()
356
    {
357
        return $this->parseDocCommentDetail(new \ReflectionClass($this));
358
    }
359
360
    /**
361
     * Returns a one-line short summary describing the specified action.
362
     * @param Action $action action to get summary for
363
     * @return string a one-line short summary describing the specified action.
364
     */
365
    public function getActionHelpSummary($action)
366
    {
367
        return $this->parseDocCommentSummary($this->getActionMethodReflection($action));
368
    }
369
370
    /**
371
     * Returns the detailed help information for the specified action.
372
     * @param Action $action action to get help for
373
     * @return string the detailed help information for the specified action.
374
     */
375
    public function getActionHelp($action)
376
    {
377
        return $this->parseDocCommentDetail($this->getActionMethodReflection($action));
378
    }
379
380
    /**
381
     * Returns the help information for the anonymous arguments for the action.
382
     * The returned value should be an array. The keys are the argument names, and the values are
383
     * the corresponding help information. Each value must be an array of the following structure:
384
     *
385
     * - required: boolean, whether this argument is required.
386
     * - type: string, the PHP type of this argument.
387
     * - default: string, the default value of this argument
388
     * - comment: string, the comment of this argument
389
     *
390
     * The default implementation will return the help information extracted from the doc-comment of
391
     * the parameters corresponding to the action method.
392
     *
393
     * @param Action $action
394
     * @return array the help information of the action arguments
395
     */
396
    public function getActionArgsHelp($action)
397
    {
398
        $method = $this->getActionMethodReflection($action);
399
        $tags = $this->parseDocCommentTags($method);
400
        $params = isset($tags['param']) ? (array) $tags['param'] : [];
401
402
        $args = [];
403
404
        /** @var \ReflectionParameter $reflection */
405
        foreach ($method->getParameters() as $i => $reflection) {
406
            $name = $reflection->getName();
407
            $tag = isset($params[$i]) ? $params[$i] : '';
408
            if (preg_match('/^(\S+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) {
409
                $type = $matches[1];
410
                $comment = $matches[3];
411
            } else {
412
                $type = null;
413
                $comment = $tag;
414
            }
415
            if ($reflection->isDefaultValueAvailable()) {
416
                $args[$name] = [
417
                    'required' => false,
418
                    'type' => $type,
419
                    'default' => $reflection->getDefaultValue(),
420
                    'comment' => $comment,
421
                ];
422
            } else {
423
                $args[$name] = [
424
                    'required' => true,
425
                    'type' => $type,
426
                    'default' => null,
427
                    'comment' => $comment,
428
                ];
429
            }
430
        }
431
        return $args;
432
    }
433
434
    /**
435
     * Returns the help information for the options for the action.
436
     * The returned value should be an array. The keys are the option names, and the values are
437
     * the corresponding help information. Each value must be an array of the following structure:
438
     *
439
     * - type: string, the PHP type of this argument.
440
     * - default: string, the default value of this argument
441
     * - comment: string, the comment of this argument
442
     *
443
     * The default implementation will return the help information extracted from the doc-comment of
444
     * the properties corresponding to the action options.
445
     *
446
     * @param Action $action
447
     * @return array the help information of the action options
448
     */
449
    public function getActionOptionsHelp($action)
450
    {
451
        $optionNames = $this->options($action->id);
452
        if (empty($optionNames)) {
453
            return [];
454
        }
455
456
        $class = new \ReflectionClass($this);
457
        $options = [];
458
        foreach ($class->getProperties() as $property) {
459
            $name = $property->getName();
460
            if (!in_array($name, $optionNames, true)) {
461
                continue;
462
            }
463
            $defaultValue = $property->getValue($this);
464
            $tags = $this->parseDocCommentTags($property);
465
            if (isset($tags['var']) || isset($tags['property'])) {
466
                $doc = isset($tags['var']) ? $tags['var'] : $tags['property'];
467
                if (is_array($doc)) {
468
                    $doc = reset($doc);
469
                }
470
                if (preg_match('/^(\S+)(.*)/s', $doc, $matches)) {
471
                    $type = $matches[1];
472
                    $comment = $matches[2];
473
                } else {
474
                    $type = null;
475
                    $comment = $doc;
476
                }
477
                $options[$name] = [
478
                    'type' => $type,
479
                    'default' => $defaultValue,
480
                    'comment' => $comment,
481
                ];
482
            } else {
483
                $options[$name] = [
484
                    'type' => null,
485
                    'default' => $defaultValue,
486
                    'comment' => '',
487
                ];
488
            }
489
        }
490
        return $options;
491
    }
492
493
    private $_reflections = [];
494
495
    /**
496
     * @param Action $action
497
     * @return \ReflectionMethod
498
     */
499
    protected function getActionMethodReflection($action)
500
    {
501
        if (!isset($this->_reflections[$action->id])) {
502
            if ($action instanceof InlineAction) {
503
                $this->_reflections[$action->id] = new \ReflectionMethod($this, $action->actionMethod);
504
            } else {
505
                $this->_reflections[$action->id] = new \ReflectionMethod($action, 'run');
506
            }
507
        }
508
        return $this->_reflections[$action->id];
509
    }
510
511
    /**
512
     * Parses the comment block into tags.
513
     * @param \Reflector $reflection the comment block
514
     * @return array the parsed tags
515
     */
516
    protected function parseDocCommentTags($reflection)
517
    {
518
        $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...
519
        $comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($comment, '/'))), "\r", '');
520
        $parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY);
521
        $tags = [];
522
        foreach ($parts as $part) {
523
            if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) {
524
                $name = $matches[1];
525
                if (!isset($tags[$name])) {
526
                    $tags[$name] = trim($matches[2]);
527
                } elseif (is_array($tags[$name])) {
528
                    $tags[$name][] = trim($matches[2]);
529
                } else {
530
                    $tags[$name] = [$tags[$name], trim($matches[2])];
531
                }
532
            }
533
        }
534
        return $tags;
535
    }
536
537
    /**
538
     * Returns the first line of docblock.
539
     *
540
     * @param \Reflector $reflection
541
     * @return string
542
     */
543
    protected function parseDocCommentSummary($reflection)
544
    {
545
        $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...
546
        if (isset($docLines[1])) {
547
            return trim($docLines[1], "\t *");
548
        }
549
        return '';
550
    }
551
552
    /**
553
     * Returns full description from the docblock.
554
     *
555
     * @param \Reflector $reflection
556
     * @return string
557
     */
558
    protected function parseDocCommentDetail($reflection)
559
    {
560
        $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...
561
        if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) {
562
            $comment = trim(substr($comment, 0, $matches[0][1]));
563
        }
564
        if ($comment !== '') {
565
            return rtrim(Console::renderColoredString(Console::markdownToAnsi($comment)));
566
        }
567
        return '';
568
    }
569
}
570