SilverstripeCommand   B
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 482
Duplicated Lines 3.32 %

Coupling/Cohesion

Components 2
Dependencies 13

Importance

Changes 4
Bugs 2 Features 1
Metric Value
wmc 40
c 4
b 2
f 1
lcom 2
cbo 13
dl 16
loc 482
rs 8.2608

28 Methods

Rating   Name   Duplication   Size   Complexity  
A run() 0 8 1
A __construct() 0 17 3
A configureUsingFluentDefinition() 0 14 3
A specifyParameters() 0 13 3
A execute() 0 11 2
fire() 0 1 ?
A call() 8 8 1
A callSilent() 8 8 1
A argument() 0 8 2
A option() 0 8 2
A confirm() 0 4 1
A ask() 0 4 1
A anticipate() 0 4 1
A askWithCompletion() 0 8 1
A secret() 0 8 1
A choice() 0 8 1
A table() 0 6 1
A info() 0 4 1
A line() 0 6 2
A comment() 0 4 1
A question() 0 4 1
A error() 0 4 1
A warn() 0 10 2
A parseVerbosity() 0 10 3
A setVerbosity() 0 4 1
A getArguments() 0 4 1
A getOptions() 0 4 1
A getOutput() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SilverstripeCommand often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SilverstripeCommand, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
use Symfony\Component\Console\Command\Command as SymfonyCommand;
4
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
5
use Symfony\Component\Console\Helper\Table;
6
use Symfony\Component\Console\Input\ArrayInput;
7
use Symfony\Component\Console\Input\InputInterface;
8
use Symfony\Component\Console\Output\NullOutput;
9
use Symfony\Component\Console\Output\OutputInterface;
10
use Symfony\Component\Console\Question\ChoiceQuestion;
11
use Symfony\Component\Console\Question\Question;
12
13
/**
14
 * Class SilverstripeCommand.
15
 *
16
 * Shameless copy/paste from Taylor Otwell's Laravel,
17
 * but modified for Silverstripe usage.
18
 *
19
 * There is not changed much, besides the removal of the Laravel application coupling.
20
 */
21
abstract class SilverstripeCommand extends SymfonyCommand
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
22
{
23
    /**
24
     * The input interface implementation.
25
     *
26
     * @var \Symfony\Component\Console\Input\InputInterface
27
     */
28
    protected $input;
29
30
    /**
31
     * The output interface implementation.
32
     *
33
     * @var \OutputStyle
34
     */
35
    protected $output;
36
37
    /**
38
     * The name and signature of the console command.
39
     *
40
     * @var string
41
     */
42
    protected $signature;
43
44
    /**
45
     * The console command name.
46
     *
47
     * @var string
48
     */
49
    protected $name;
50
51
    /**
52
     * The console command description.
53
     *
54
     * @var string
55
     */
56
    protected $description;
57
58
    /**
59
     * The default verbosity of output commands.
60
     *
61
     * @var int
62
     */
63
    protected $verbosity = OutputInterface::VERBOSITY_NORMAL;
64
65
    /**
66
     * The mapping between human readable verbosity levels and Symfony's OutputInterface.
67
     *
68
     * @var array
69
     */
70
    protected $verbosityMap = [
71
        'v'      => OutputInterface::VERBOSITY_VERBOSE,
72
        'vv'     => OutputInterface::VERBOSITY_VERY_VERBOSE,
73
        'vvv'    => OutputInterface::VERBOSITY_DEBUG,
74
        'quiet'  => OutputInterface::VERBOSITY_QUIET,
75
        'normal' => OutputInterface::VERBOSITY_NORMAL,
76
    ];
77
78
    /**
79
     * Create a new console command instance.
80
     *
81
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
82
     */
83
    public function __construct()
84
    {
85
        // We will go ahead and set the name, description, and parameters on console
86
        // commands just to make things a little easier on the developer. This is
87
        // so they don't have to all be manually specified in the constructors.
88
        if (isset($this->signature)) {
89
            $this->configureUsingFluentDefinition();
90
        } else {
91
            parent::__construct($this->name);
92
        }
93
94
        $this->setDescription($this->description);
95
96
        if (!isset($this->signature)) {
97
            $this->specifyParameters();
98
        }
99
    }
100
101
    /**
102
     * Configure the console command using a fluent definition.
103
     *
104
     * @return void
105
     */
106
    protected function configureUsingFluentDefinition()
107
    {
108
        list($name, $arguments, $options) = \CommandParser::parse($this->signature);
109
110
        parent::__construct($name);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (__construct() instead of configureUsingFluentDefinition()). Are you sure this is correct? If so, you might want to change this to $this->__construct().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
111
112
        foreach ($arguments as $argument) {
113
            $this->getDefinition()->addArgument($argument);
114
        }
115
116
        foreach ($options as $option) {
117
            $this->getDefinition()->addOption($option);
118
        }
119
    }
120
121
    /**
122
     * Specify the arguments and options on the command.
123
     *
124
     * @return void
125
     */
126
    protected function specifyParameters()
127
    {
128
        // We will loop through all of the arguments and options for the command and
129
        // set them all on the base command instance. This specifies what can get
130
        // passed into these commands as "parameters" to control the execution.
131
        foreach ($this->getArguments() as $arguments) {
132
            call_user_func_array([$this, 'addArgument'], $arguments);
133
        }
134
135
        foreach ($this->getOptions() as $options) {
136
            call_user_func_array([$this, 'addOption'], $options);
137
        }
138
    }
139
140
    /**
141
     * Run the console command.
142
     *
143
     * @param \Symfony\Component\Console\Input\InputInterface   $input
144
     * @param \Symfony\Component\Console\Output\OutputInterface $output
145
     *
146
     * @return int
147
     */
148
    public function run(InputInterface $input, OutputInterface $output)
149
    {
150
        $this->input = $input;
151
152
        $this->output = new OutputStyle($input, $output);
153
154
        return parent::run($input, $output);
155
    }
156
157
    /**
158
     * Execute the console command.
159
     *
160
     * @param \Symfony\Component\Console\Input\InputInterface   $input
161
     * @param \Symfony\Component\Console\Output\OutputInterface $output
162
     *
163
     * @return mixed
164
     */
165
    protected function execute(InputInterface $input, OutputInterface $output)
166
    {
167
        $this->fire();
168
169
        // check last, so message is always visible
170
        $checker = new SuperSakeChecker();
171
        $msg = $checker->superSakeIsNotProtected();
172
        if ((bool) $msg) {
173
            $this->error($msg);
0 ignored issues
show
Documentation introduced by
$msg is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
174
        }
175
    }
176
177
    abstract public function fire();
0 ignored issues
show
Documentation introduced by
For interfaces and abstract methods it is generally a good practice to add a @return annotation even if it is just @return void or @return null, so that implementors know what to do in the overridden method.

For interface and abstract methods, it is impossible to infer the return type from the immediate code. In these cases, it is generally advisible to explicitly annotate these methods with a @return doc comment to communicate to implementors of these methods what they are expected to return.

Loading history...
178
179
    /**
180
     * Call another console command.
181
     *
182
     * @param string $command
183
     * @param array  $arguments
184
     *
185
     * @return int
186
     */
187 View Code Duplication
    public function call($command, array $arguments = [])
188
    {
189
        $instance = $this->getApplication()->find($command);
190
191
        $arguments['command'] = $command;
192
193
        return $instance->run(new ArrayInput($arguments), $this->output);
194
    }
195
196
    /**
197
     * Call another console command silently.
198
     *
199
     * @param string $command
200
     * @param array  $arguments
201
     *
202
     * @return int
203
     */
204 View Code Duplication
    public function callSilent($command, array $arguments = [])
205
    {
206
        $instance = $this->getApplication()->find($command);
207
208
        $arguments['command'] = $command;
209
210
        return $instance->run(new ArrayInput($arguments), new NullOutput());
211
    }
212
213
    /**
214
     * Get the value of a command argument.
215
     *
216
     * @param string $key
0 ignored issues
show
Documentation introduced by
Should the type for parameter $key not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
217
     *
218
     * @return string|array
219
     */
220
    public function argument($key = null)
221
    {
222
        if (is_null($key)) {
223
            return $this->input->getArguments();
224
        }
225
226
        return $this->input->getArgument($key);
227
    }
228
229
    /**
230
     * Get the value of a command option.
231
     *
232
     * @param string $key
0 ignored issues
show
Documentation introduced by
Should the type for parameter $key not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
233
     *
234
     * @return string|array
235
     */
236
    public function option($key = null)
237
    {
238
        if (is_null($key)) {
239
            return $this->input->getOptions();
240
        }
241
242
        return $this->input->getOption($key);
243
    }
244
245
    /**
246
     * Confirm a question with the user.
247
     *
248
     * @param string $question
249
     * @param bool   $default
250
     *
251
     * @return bool
0 ignored issues
show
Documentation introduced by
Should the return type not be string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
252
     */
253
    public function confirm($question, $default = false)
254
    {
255
        return $this->output->confirm($question, $default);
256
    }
257
258
    /**
259
     * Prompt the user for input.
260
     *
261
     * @param string $question
262
     * @param string $default
0 ignored issues
show
Documentation introduced by
Should the type for parameter $default not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
263
     *
264
     * @return string
265
     */
266
    public function ask($question, $default = null)
267
    {
268
        return $this->output->ask($question, $default);
269
    }
270
271
    /**
272
     * Prompt the user for input with auto completion.
273
     *
274
     * @param string $question
275
     * @param array  $choices
276
     * @param string $default
0 ignored issues
show
Documentation introduced by
Should the type for parameter $default not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
277
     *
278
     * @return string
279
     */
280
    public function anticipate($question, array $choices, $default = null)
281
    {
282
        return $this->askWithCompletion($question, $choices, $default);
283
    }
284
285
    /**
286
     * Prompt the user for input with auto completion.
287
     *
288
     * @param string $question
289
     * @param array  $choices
290
     * @param string $default
0 ignored issues
show
Documentation introduced by
Should the type for parameter $default not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
291
     *
292
     * @return string
293
     */
294
    public function askWithCompletion($question, array $choices, $default = null)
295
    {
296
        $question = new Question($question, $default);
297
298
        $question->setAutocompleterValues($choices);
299
300
        return $this->output->askQuestion($question);
301
    }
302
303
    /**
304
     * Prompt the user for input but hide the answer from the console.
305
     *
306
     * @param string $question
307
     * @param bool   $fallback
308
     *
309
     * @return string
310
     */
311
    public function secret($question, $fallback = true)
312
    {
313
        $question = new Question($question);
314
315
        $question->setHidden(true)->setHiddenFallback($fallback);
316
317
        return $this->output->askQuestion($question);
318
    }
319
320
    /**
321
     * Give the user a single choice from an array of answers.
322
     *
323
     * @param string $question
324
     * @param array  $choices
325
     * @param string $default
0 ignored issues
show
Documentation introduced by
Should the type for parameter $default not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
326
     * @param mixed  $attempts
327
     * @param bool   $multiple
0 ignored issues
show
Documentation introduced by
Should the type for parameter $multiple not be boolean|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
328
     *
329
     * @return string
330
     */
331
    public function choice($question, array $choices, $default = null, $attempts = null, $multiple = null)
332
    {
333
        $question = new ChoiceQuestion($question, $choices, $default);
334
335
        $question->setMaxAttempts($attempts)->setMultiselect($multiple);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\Console\Question\Question as the method setMultiselect() does only exist in the following sub-classes of Symfony\Component\Console\Question\Question: Symfony\Component\Console\Question\ChoiceQuestion. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
336
337
        return $this->output->askQuestion($question);
338
    }
339
340
    /**
341
     * Format input to textual table.
342
     *
343
     * @param array  $headers
344
     * @param array  $rows
345
     * @param string $style
346
     *
347
     * @return void
348
     */
349
    public function table(array $headers, $rows, $style = 'default')
350
    {
351
        $table = new Table($this->output);
352
353
        $table->setHeaders($headers)->setRows($rows)->setStyle($style)->render();
354
    }
355
356
    /**
357
     * Write a string as information output.
358
     *
359
     * @param string          $string
360
     * @param null|int|string $verbosity
361
     *
362
     * @return void
363
     */
364
    public function info($string, $verbosity = null)
365
    {
366
        $this->line($string, 'info', $verbosity);
367
    }
368
369
    /**
370
     * Write a string as standard output.
371
     *
372
     * @param string          $string
373
     * @param string          $style
0 ignored issues
show
Documentation introduced by
Should the type for parameter $style not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
374
     * @param null|int|string $verbosity
375
     *
376
     * @return void
377
     */
378
    public function line($string, $style = null, $verbosity = null)
379
    {
380
        $styled = $style ? "<$style>$string</$style>" : $string;
381
382
        $this->output->writeln($styled, $this->parseVerbosity($verbosity));
383
    }
384
385
    /**
386
     * Write a string as comment output.
387
     *
388
     * @param string          $string
389
     * @param null|int|string $verbosity
390
     *
391
     * @return void
392
     */
393
    public function comment($string, $verbosity = null)
394
    {
395
        $this->line($string, 'comment', $verbosity);
396
    }
397
398
    /**
399
     * Write a string as question output.
400
     *
401
     * @param string          $string
402
     * @param null|int|string $verbosity
403
     *
404
     * @return void
405
     */
406
    public function question($string, $verbosity = null)
407
    {
408
        $this->line($string, 'question', $verbosity);
409
    }
410
411
    /**
412
     * Write a string as error output.
413
     *
414
     * @param string          $string
415
     * @param null|int|string $verbosity
416
     *
417
     * @return void
418
     */
419
    public function error($string, $verbosity = null)
420
    {
421
        $this->line($string, 'error', $verbosity);
422
    }
423
424
    /**
425
     * Write a string as warning output.
426
     *
427
     * @param string          $string
428
     * @param null|int|string $verbosity
429
     *
430
     * @return void
431
     */
432
    public function warn($string, $verbosity = null)
433
    {
434
        if (!$this->output->getFormatter()->hasStyle('warning')) {
435
            $style = new OutputFormatterStyle('yellow');
436
437
            $this->output->getFormatter()->setStyle('warning', $style);
438
        }
439
440
        $this->line($string, 'warning', $verbosity);
441
    }
442
443
    /**
444
     * Get the verbosity level in terms of Symfony's OutputInterface level.
445
     *
446
     * @param string|int $level
0 ignored issues
show
Documentation introduced by
Should the type for parameter $level not be string|integer|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
447
     *
448
     * @return int
449
     */
450
    protected function parseVerbosity($level = null)
451
    {
452
        if (isset($this->verbosityMap[$level])) {
453
            $level = $this->verbosityMap[$level];
454
        } elseif (!is_int($level)) {
455
            $level = $this->verbosity;
456
        }
457
458
        return $level;
459
    }
460
461
    /**
462
     * Set the verbosity level.
463
     *
464
     * @param string|int $level
465
     *
466
     * @return void
467
     */
468
    protected function setVerbosity($level)
469
    {
470
        $this->verbosity = $this->parseVerbosity($level);
471
    }
472
473
    /**
474
     * Get the console command arguments.
475
     *
476
     * @return array
477
     */
478
    protected function getArguments()
479
    {
480
        return [];
481
    }
482
483
    /**
484
     * Get the console command options.
485
     *
486
     * @return array
487
     */
488
    protected function getOptions()
489
    {
490
        return [];
491
    }
492
493
    /**
494
     * Get the output implementation.
495
     *
496
     * @return \Symfony\Component\Console\Output\OutputInterface
497
     */
498
    public function getOutput()
499
    {
500
        return $this->output;
501
    }
502
}
503