Completed
Pull Request — master (#10457)
by Sam
07:12
created

Controller::select()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2
Metric Value
dl 0
loc 4
rs 10
ccs 0
cts 2
cp 0
cc 1
eloc 2
nc 1
nop 2
crap 2
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\console;
9
10
use Yii;
11
use yii\base\Action;
12
use yii\base\InlineAction;
13
use yii\base\InvalidRouteException;
14
use yii\helpers\Console;
15
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
        $params = array_values($params);
126
127 42
        $args = [];
128 42
        $missing = [];
129 42
        foreach ($method->getParameters() as $param) {
130 42
            if (($class = $param->getClass()) !== null) {
131 1
                $name = $param->getName();
132 1
                $className = $class->getName();
133 1
                if (Yii::$app->has($name) && ($obj = Yii::$app->get($name)) instanceof $className) {
134 1
                    $args[] = $obj;
135 1
                } else {
136 1
                    $args[] = Yii::$container->get($className);
137
                }
138 1
                continue;
139
            }
140 42
            $value = array_shift($params);
141 42
            if (isset($value)) {
142 38
                $args[] = $param->isArray() ? preg_split('/\s*,\s*/', $value) : $value;
143 38
            } else {
144 7
                if ($param->isDefaultValueAvailable()) {
145 7
                    $args[] = $param->getDefaultValue();
146 7
                } else {
147 1
                    $missing[] = $param->getName();
148
                }
149
            }
150 42
        }
151
152 42
        if (!empty($missing)) {
153 1
            throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', $missing)]));
154
        }
155
156 42
        foreach ($params as $value) {
157 1
            $args[] = $value;
158 42
        }
159 42
        return $args;
160
    }
161
162
    /**
163
     * Formats a string with ANSI codes
164
     *
165
     * You may pass additional parameters using the constants defined in [[\yii\helpers\Console]].
166
     *
167
     * Example:
168
     *
169
     * ```
170
     * echo $this->ansiFormat('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
171
     * ```
172
     *
173
     * @param string $string the string to be formatted
174
     * @return string
175
     */
176
    public function ansiFormat($string)
177
    {
178
        if ($this->isColorEnabled()) {
179
            $args = func_get_args();
180
            array_shift($args);
181
            $string = Console::ansiFormat($string, $args);
182
        }
183
        return $string;
184
    }
185
186
    /**
187
     * Prints a string to STDOUT
188
     *
189
     * You may optionally format the string with ANSI codes by
190
     * passing additional parameters using the constants defined in [[\yii\helpers\Console]].
191
     *
192
     * Example:
193
     *
194
     * ```
195
     * $this->stdout('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
196
     * ```
197
     *
198
     * @param string $string the string to print
199
     * @return int|boolean Number of bytes printed or false on error
200
     */
201
    public function stdout($string)
202
    {
203
        if ($this->isColorEnabled()) {
204
            $args = func_get_args();
205
            array_shift($args);
206
            $string = Console::ansiFormat($string, $args);
207
        }
208
        return Console::stdout($string);
209
    }
210
211
    /**
212
     * Prints a string to STDERR
213
     *
214
     * You may optionally format the string with ANSI codes by
215
     * passing additional parameters using the constants defined in [[\yii\helpers\Console]].
216
     *
217
     * Example:
218
     *
219
     * ```
220
     * $this->stderr('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE);
221
     * ```
222
     *
223
     * @param string $string the string to print
224
     * @return int|boolean Number of bytes printed or false on error
225
     */
226
    public function stderr($string)
227
    {
228
        if ($this->isColorEnabled(\STDERR)) {
229
            $args = func_get_args();
230
            array_shift($args);
231
            $string = Console::ansiFormat($string, $args);
232
        }
233
        return fwrite(\STDERR, $string);
234
    }
235
236
    /**
237
     * Prompts the user for input and validates it
238
     *
239
     * @param string $text prompt string
240
     * @param array $options the options to validate the input:
241
     *
242
     *  - required: whether it is required or not
243
     *  - default: default value if no input is inserted by the user
244
     *  - pattern: regular expression pattern to validate user input
245
     *  - validator: a callable function to validate input. The function must accept two parameters:
246
     *      - $input: the user input to validate
247
     *      - $error: the error value passed by reference if validation failed.
248
     * @return string the user input
249
     */
250
    public function prompt($text, $options = [])
251
    {
252
        if ($this->interactive) {
253
            return Console::prompt($text, $options);
254
        } else {
255
            return isset($options['default']) ? $options['default'] : '';
256
        }
257
    }
258
259
    /**
260
     * Asks user to confirm by typing y or n.
261
     *
262
     * @param string $message to echo out before waiting for user input
263
     * @param boolean $default this value is returned if no selection is made.
264
     * @return boolean whether user confirmed.
265
     * Will return true if [[interactive]] is false.
266
     */
267 26
    public function confirm($message, $default = false)
268
    {
269 26
        if ($this->interactive) {
270
            return Console::confirm($message, $default);
271
        } else {
272 26
            return true;
273
        }
274
    }
275
276
    /**
277
     * Gives the user an option to choose from. Giving '?' as an input will show
278
     * a list of options to choose from and their explanations.
279
     *
280
     * @param string $prompt the prompt message
281
     * @param array $options Key-value array of options to choose from
282
     *
283
     * @return string An option character the user chose
284
     */
285
    public function select($prompt, $options = [])
286
    {
287
        return Console::select($prompt, $options);
288
    }
289
290
    /**
291
     * Returns the names of valid options for the action (id)
292
     * An option requires the existence of a public member variable whose
293
     * name is the option name.
294
     * Child classes may override this method to specify possible options.
295
     *
296
     * Note that the values setting via options are not available
297
     * until [[beforeAction()]] is being called.
298
     *
299
     * @param string $actionID the action id of the current request
300
     * @return array the names of the options valid for the action
301
     */
302 16
    public function options($actionID)
303
    {
304
        // $actionId might be used in subclasses to provide options specific to action id
305 16
        return ['color', 'interactive'];
306
    }
307
308
    /**
309
     * Returns properties corresponding to the options for the action id
310
     * Child classes may override this method to specify possible properties.
311
     *
312
     * @param string $actionID the action id of the current request
313
     * @return array properties corresponding to the options for the action
314
     */
315 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...
316
    {
317
        // $actionId might be used in subclasses to provide properties specific to action id
318 20
        $properties = [];
319 20
        foreach ($this->options($this->action->id) as $property) {
320 20
            $properties[$property] = $this->$property;
321 20
        }
322 20
        return $properties;
323
    }
324
325
    /**
326
     * Returns the names of valid options passed during execution.
327
     *
328
     * @return array the names of the options passed during execution
329
     */
330
    public function getPassedOptions()
331
    {
332
        return $this->_passedOptions;
333
    }
334
335
    /**
336
     * Returns the properties corresponding to the passed options
337
     *
338
     * @return array the properties corresponding to the passed options
339
     */
340 18
    public function getPassedOptionValues()
341
    {
342 18
        $properties = [];
343 18
        foreach ($this->_passedOptions as $property) {
344
            $properties[$property] = $this->$property;
345 18
        }
346 18
        return $properties;
347
    }
348
349
    /**
350
     * Returns one-line short summary describing this controller.
351
     *
352
     * You may override this method to return customized summary.
353
     * The default implementation returns first line from the PHPDoc comment.
354
     *
355
     * @return string
356
     */
357
    public function getHelpSummary()
358
    {
359
        return $this->parseDocCommentSummary(new \ReflectionClass($this));
360
    }
361
362
    /**
363
     * Returns help information for this controller.
364
     *
365
     * You may override this method to return customized help.
366
     * The default implementation returns help information retrieved from the PHPDoc comment.
367
     * @return string
368
     */
369
    public function getHelp()
370
    {
371
        return $this->parseDocCommentDetail(new \ReflectionClass($this));
372
    }
373
374
    /**
375
     * Returns a one-line short summary describing the specified action.
376
     * @param Action $action action to get summary for
377
     * @return string a one-line short summary describing the specified action.
378
     */
379
    public function getActionHelpSummary($action)
380
    {
381
        return $this->parseDocCommentSummary($this->getActionMethodReflection($action));
382
    }
383
384
    /**
385
     * Returns the detailed help information for the specified action.
386
     * @param Action $action action to get help for
387
     * @return string the detailed help information for the specified action.
388
     */
389
    public function getActionHelp($action)
390
    {
391
        return $this->parseDocCommentDetail($this->getActionMethodReflection($action));
392
    }
393
394
    /**
395
     * Returns the help information for the anonymous arguments for the action.
396
     * The returned value should be an array. The keys are the argument names, and the values are
397
     * the corresponding help information. Each value must be an array of the following structure:
398
     *
399
     * - required: boolean, whether this argument is required.
400
     * - type: string, the PHP type of this argument.
401
     * - default: string, the default value of this argument
402
     * - comment: string, the comment of this argument
403
     *
404
     * The default implementation will return the help information extracted from the doc-comment of
405
     * the parameters corresponding to the action method.
406
     *
407
     * @param Action $action
408
     * @return array the help information of the action arguments
409
     */
410 1
    public function getActionArgsHelp($action)
411
    {
412 1
        $method = $this->getActionMethodReflection($action);
413 1
        $tags = $this->parseDocCommentTags($method);
414 1
        $params = isset($tags['param']) ? (array) $tags['param'] : [];
415
416 1
        $args = [];
417
418
        /** @var \ReflectionParameter $reflection */
419 1
        foreach ($method->getParameters() as $i => $reflection) {
420 1
            $name = $reflection->getName();
421 1
            if ($reflection->getClass() !== null) continue;
422 1
            $tag = isset($params[$i]) ? $params[$i] : '';
423 1
            if (preg_match('/^(\S+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) {
424
                $type = $matches[1];
425
                $comment = $matches[3];
426
            } else {
427 1
                $type = null;
428 1
                $comment = $tag;
429
            }
430 1
            if ($reflection->isDefaultValueAvailable()) {
431 1
                $args[$name] = [
432 1
                    'required' => false,
433 1
                    'type' => $type,
434 1
                    'default' => $reflection->getDefaultValue(),
435 1
                    'comment' => $comment,
436
                ];
437 1
            } else {
438 1
                $args[$name] = [
439 1
                    'required' => true,
440 1
                    'type' => $type,
441 1
                    'default' => null,
442 1
                    'comment' => $comment,
443
                ];
444
            }
445 1
        }
446 1
        return $args;
447
    }
448
449
    /**
450
     * Returns the help information for the options for the action.
451
     * The returned value should be an array. The keys are the option names, and the values are
452
     * the corresponding help information. Each value must be an array of the following structure:
453
     *
454
     * - type: string, the PHP type of this argument.
455
     * - default: string, the default value of this argument
456
     * - comment: string, the comment of this argument
457
     *
458
     * The default implementation will return the help information extracted from the doc-comment of
459
     * the properties corresponding to the action options.
460
     *
461
     * @param Action $action
462
     * @return array the help information of the action options
463
     */
464
    public function getActionOptionsHelp($action)
465
    {
466
        $optionNames = $this->options($action->id);
467
        if (empty($optionNames)) {
468
            return [];
469
        }
470
471
        $class = new \ReflectionClass($this);
472
        $options = [];
473
        foreach ($class->getProperties() as $property) {
474
            $name = $property->getName();
475
            if (!in_array($name, $optionNames, true)) {
476
                continue;
477
            }
478
            $defaultValue = $property->getValue($this);
479
            $tags = $this->parseDocCommentTags($property);
480
            if (isset($tags['var']) || isset($tags['property'])) {
481
                $doc = isset($tags['var']) ? $tags['var'] : $tags['property'];
482
                if (is_array($doc)) {
483
                    $doc = reset($doc);
484
                }
485
                if (preg_match('/^(\S+)(.*)/s', $doc, $matches)) {
486
                    $type = $matches[1];
487
                    $comment = $matches[2];
488
                } else {
489
                    $type = null;
490
                    $comment = $doc;
491
                }
492
                $options[$name] = [
493
                    'type' => $type,
494
                    'default' => $defaultValue,
495
                    'comment' => $comment,
496
                ];
497
            } else {
498
                $options[$name] = [
499
                    'type' => null,
500
                    'default' => $defaultValue,
501
                    'comment' => '',
502
                ];
503
            }
504
        }
505
        return $options;
506
    }
507
508
    private $_reflections = [];
509
510
    /**
511
     * @param Action $action
512
     * @return \ReflectionMethod
513
     */
514 1
    protected function getActionMethodReflection($action)
515
    {
516 1
        if (!isset($this->_reflections[$action->id])) {
517 1
            if ($action instanceof InlineAction) {
518 1
                $this->_reflections[$action->id] = new \ReflectionMethod($this, $action->actionMethod);
519 1
            } else {
520
                $this->_reflections[$action->id] = new \ReflectionMethod($action, 'run');
521
            }
522 1
        }
523 1
        return $this->_reflections[$action->id];
524
    }
525
526
    /**
527
     * Parses the comment block into tags.
528
     * @param \Reflector $reflection the comment block
529
     * @return array the parsed tags
530
     */
531 1
    protected function parseDocCommentTags($reflection)
532
    {
533 1
        $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...
534 1
        $comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($comment, '/'))), "\r", '');
535 1
        $parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY);
536 1
        $tags = [];
537 1
        foreach ($parts as $part) {
538 1
            if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) {
539 1
                $name = $matches[1];
540 1
                if (!isset($tags[$name])) {
541 1
                    $tags[$name] = trim($matches[2]);
542 1
                } elseif (is_array($tags[$name])) {
543
                    $tags[$name][] = trim($matches[2]);
544
                } else {
545
                    $tags[$name] = [$tags[$name], trim($matches[2])];
546
                }
547 1
            }
548 1
        }
549 1
        return $tags;
550
    }
551
552
    /**
553
     * Returns the first line of docblock.
554
     *
555
     * @param \Reflector $reflection
556
     * @return string
557
     */
558
    protected function parseDocCommentSummary($reflection)
559
    {
560
        $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...
561
        if (isset($docLines[1])) {
562
            return trim($docLines[1], "\t *");
563
        }
564
        return '';
565
    }
566
567
    /**
568
     * Returns full description from the docblock.
569
     *
570
     * @param \Reflector $reflection
571
     * @return string
572
     */
573
    protected function parseDocCommentDetail($reflection)
574
    {
575
        $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...
576
        if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) {
577
            $comment = trim(substr($comment, 0, $matches[0][1]));
578
        }
579
        if ($comment !== '') {
580
            return rtrim(Console::renderColoredString(Console::markdownToAnsi($comment)));
581
        }
582
        return '';
583
    }
584
}
585