Command::getApplication()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
/*
4
 * This file is part of the webmozart/console package.
5
 *
6
 * (c) Bernhard Schussek <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Webmozart\Console\Api\Command;
13
14
use Exception;
15
use LogicException;
16
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
17
use Webmozart\Console\Api\Application\Application;
18
use Webmozart\Console\Api\Args\Args;
19
use Webmozart\Console\Api\Args\CannotParseArgsException;
20
use Webmozart\Console\Api\Args\Format\ArgsFormat;
21
use Webmozart\Console\Api\Args\RawArgs;
22
use Webmozart\Console\Api\Config\CommandConfig;
23
use Webmozart\Console\Api\Config\OptionCommandConfig;
24
use Webmozart\Console\Api\Config\SubCommandConfig;
25
use Webmozart\Console\Api\Event\ConsoleEvents;
26
use Webmozart\Console\Api\Event\PreHandleEvent;
27
use Webmozart\Console\Api\IO\IO;
28
use Webmozart\Console\Util\ProcessTitle;
29
30
/**
31
 * A console command.
32
 *
33
 * A `Command` object contains all the information that is necessary to describe
34
 * and run a console command. Use the {@link CommandConfig} class to configure
35
 * a command:
36
 *
37
 * ```php
38
 * $config = CommandConfig::create()
39
 *     ->setDescription('List and manage servers')
40
 *
41
 *     ->beginSubCommand('add')
42
 *         ->setDescription('Add a new server')
43
 *         ->addArgument('host', InputArgument::REQUIRED)
44
 *         ->addOption('port', 'p', InputOption::VALUE_OPTIONAL, null, 80)
45
 *     ->end()
46
 * ;
47
 *
48
 * $command = new Command($config);
49
 * ```
50
 *
51
 * @since  1.0
52
 *
53
 * @author Bernhard Schussek <[email protected]>
54
 */
55
class Command
56
{
57
    /**
58
     * @var string
59
     */
60
    private $name;
61
62
    /**
63
     * @var string
64
     */
65
    private $shortName;
66
67
    /**
68
     * @var string[]
69
     */
70
    private $aliases = array();
71
72
    /**
73
     * @var CommandConfig
74
     */
75
    private $config;
76
77
    /**
78
     * @var ArgsFormat
79
     */
80
    private $argsFormat;
81
82
    /**
83
     * @var Application
84
     */
85
    private $application;
86
87
    /**
88
     * @var Command
89
     */
90
    private $parentCommand;
91
92
    /**
93
     * @var CommandCollection
94
     */
95
    private $subCommands;
96
97
    /**
98
     * @var CommandCollection
99
     */
100
    private $namedSubCommands;
101
102
    /**
103
     * @var CommandCollection
104
     */
105
    private $defaultSubCommands;
106
107
    /**
108
     * @var EventDispatcherInterface
109
     */
110
    private $dispatcher;
111
112
    /**
113
     * Creates a new command.
114
     *
115
     * @param CommandConfig $config        The command configuration.
116
     * @param Application   $application   The console application.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $application not be null|Application?

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...
117
     * @param Command       $parentCommand The parent command.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $parentCommand not be null|Command?

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...
118
     *
119
     * @throws LogicException If the name of the command configuration is not set.
120
     */
121 230
    public function __construct(CommandConfig $config, Application $application = null, Command $parentCommand = null)
122
    {
123 230
        if (!$config->getName()) {
124 1
            throw new LogicException('The name of the command config must be set.');
125
        }
126
127 229
        $this->name = $config->getName();
128 229
        $this->shortName = $config instanceof OptionCommandConfig ? $config->getShortName() : null;
129 229
        $this->aliases = $config->getAliases();
130 229
        $this->config = $config;
131 229
        $this->application = $application;
132 229
        $this->parentCommand = $parentCommand;
133 229
        $this->subCommands = new CommandCollection();
134 229
        $this->namedSubCommands = new CommandCollection();
135 229
        $this->defaultSubCommands = new CommandCollection();
136 229
        $this->argsFormat = $config->buildArgsFormat($this->getBaseFormat());
137 229
        $this->dispatcher = $application ? $application->getConfig()->getEventDispatcher() : null;
138
139 229
        foreach ($config->getSubCommandConfigs() as $subConfig) {
140 32
            $this->addSubCommand($subConfig);
141
        }
142 226
    }
143
144
    /**
145
     * Returns the name of the command.
146
     *
147
     * @return string The name of the command.
148
     */
149 360
    public function getName()
150
    {
151 360
        return $this->name;
152
    }
153
154
    /**
155
     * Returns the short name of the command.
156
     *
157
     * This method only returns a value if an {@link OptionCommandConfig} was
158
     * passed to the constructor. Otherwise this method returns `null`.
159
     *
160
     * @return string|null The short name or `null` if the command is not an
161
     *                     option command.
162
     */
163 177
    public function getShortName()
164
    {
165 177
        return $this->shortName;
166
    }
167
168
    /**
169
     * Returns the alias names of the command.
170
     *
171
     * @return string[] An array of alias names of the command.
172
     */
173 179
    public function getAliases()
174
    {
175 179
        return $this->aliases;
176
    }
177
178
    /**
179
     * Returns whether the command has aliases.
180
     *
181
     * @return bool Returns `true` if the command has aliases and `false`
182
     *              otherwise.
183
     */
184 27
    public function hasAliases()
185
    {
186 27
        return count($this->aliases) > 0;
187
    }
188
189
    /**
190
     * Returns the configuration of the command.
191
     *
192
     * @return CommandConfig The command configuration.
193
     */
194 253
    public function getConfig()
195
    {
196 253
        return $this->config;
197
    }
198
199
    /**
200
     * Returns the arguments format of the command.
201
     *
202
     * @return ArgsFormat The input definition.
203
     */
204 71
    public function getArgsFormat()
205
    {
206 71
        return $this->argsFormat;
207
    }
208
209
    /**
210
     * Returns the console application.
211
     *
212
     * @return Application The console application.
213
     */
214 79
    public function getApplication()
215
    {
216 79
        return $this->application;
217
    }
218
219
    /**
220
     * Returns the parent command.
221
     *
222
     * @return Command The parent command.
223
     */
224 1
    public function getParentCommand()
225
    {
226 1
        return $this->parentCommand;
227
    }
228
229
    /**
230
     * Returns all sub-commands of the command.
231
     *
232
     * @return CommandCollection The sub-commands.
233
     */
234 29
    public function getSubCommands()
235
    {
236 29
        return $this->subCommands;
237
    }
238
239
    /**
240
     * Returns the sub-command with a specific name.
241
     *
242
     * @param string $name The name of the sub-command.
243
     *
244
     * @return Command The sub-command.
245
     *
246
     * @throws NoSuchCommandException If the sub-command with the given name
247
     *                                does not exist.
248
     */
249 3
    public function getSubCommand($name)
250
    {
251 3
        return $this->subCommands->get($name);
252
    }
253
254
    /**
255
     * Returns whether a sub-command with the given name exists.
256
     *
257
     * @param string $name The name of the sub-command.
258
     *
259
     * @return bool Returns `true` if a sub-command with that name exists and
260
     *              `false` otherwise.
261
     */
262 1
    public function hasSubCommand($name)
263
    {
264 1
        return $this->subCommands->contains($name);
265
    }
266
267
    /**
268
     * Returns whether the command has any sub-commands.
269
     *
270
     * @return bool Returns `true` if the command has sub-commands and `false`
271
     *              otherwise.
272
     */
273 2
    public function hasSubCommands()
274
    {
275 2
        return !$this->subCommands->isEmpty();
276
    }
277
278
    /**
279
     * Returns all sub-commands that are not anonymous.
280
     *
281
     * @return CommandCollection The named commands.
282
     */
283 226
    public function getNamedSubCommands()
284
    {
285 226
        return $this->namedSubCommands;
286
    }
287
288
    /**
289
     * Returns whether the command has any commands that are not anonymous.
290
     *
291
     * @return bool Returns `true` if the command has named commands and
292
     *              `false` otherwise.
293
     *
294
     * @see getNamedSubCommands()
295
     */
296 2
    public function hasNamedSubCommands()
297
    {
298 2
        return count($this->namedSubCommands) > 0;
299
    }
300
301
    /**
302
     * Returns the commands that should be executed if no explicit command is
303
     * passed.
304
     *
305
     * @return CommandCollection The default commands.
306
     */
307 204
    public function getDefaultSubCommands()
308
    {
309 204
        return $this->defaultSubCommands;
310
    }
311
312
    /**
313
     * Returns whether the command has any default commands.
314
     *
315
     * @return bool Returns `true` if the command has default commands and
316
     *              `false` otherwise.
317
     *
318
     * @see getDefaultSubCommands()
319
     */
320 29
    public function hasDefaultSubCommands()
321
    {
322 29
        return count($this->defaultSubCommands) > 0;
323
    }
324
325
    /**
326
     * Parses the raw console arguments and returns the parsed arguments.
327
     *
328
     * @param RawArgs $args    The raw console arguments.
329
     * @param bool    $lenient Whether the parser should ignore parse errors.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $lenient 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...
330
     *                         If `true`, the parser will not throw any
331
     *                         exceptions when parse errors occur.
332
     *
333
     * @return Args The parsed console arguments.
334
     *
335
     * @throws CannotParseArgsException If the arguments cannot be parsed.
336
     */
337 268
    public function parseArgs(RawArgs $args, $lenient = null)
338
    {
339 268
        if (null === $lenient) {
340 260
            $lenient = $this->config->isLenientArgsParsingEnabled();
341
        }
342
343 268
        return $this->config->getArgsParser()->parseArgs($args, $this->argsFormat, $lenient);
344
    }
345
346
    /**
347
     * Executes the command for the given unparsed arguments.
348
     *
349
     * @param RawArgs $args The unparsed console arguments.
350
     * @param IO      $io   The I/O.
351
     *
352
     * @return int Returns 0 on success and any other integer on error.
353
     */
354 2
    public function run(RawArgs $args, IO $io)
355
    {
356 2
        return $this->handle($this->parseArgs($args), $io);
357
    }
358
359
    /**
360
     * Executes the command for the given parsed arguments.
361
     *
362
     * @param Args $args The parsed console arguments.
363
     * @param IO   $io   The I/O.
364
     *
365
     * @return int Returns 0 on success and any other integer on error.
366
     *
367
     * @throws Exception
368
     */
369 50
    public function handle(Args $args, IO $io)
370
    {
371 50
        $processTitle = $this->config->getProcessTitle();
372
373 50
        $this->warnIfProcessTitleNotSupported($processTitle, $io);
374
375 50
        if ($processTitle && ProcessTitle::isSupported()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $processTitle of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
376
            ProcessTitle::setProcessTitle($processTitle);
377
378
            try {
379
                $statusCode = $this->doHandle($args, $io);
380
            } catch (Exception $e) {
381
                ProcessTitle::resetProcessTitle();
382
383
                throw $e;
384
            }
385
386
            ProcessTitle::resetProcessTitle();
387
        } else {
388 50
            $statusCode = $this->doHandle($args, $io);
389
        }
390
391
        // Any empty value is considered a success
392 46
        if (!$statusCode) {
393 16
            return 0;
394
        }
395
396
        // Anything else is normalized to a valid error status code
397
        // (i.e. one of [1, 255])
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
398 30
        return min(max((int) $statusCode, 1), 255);
399
    }
400
401
    /**
402
     * Returns the inherited arguments format of the command.
403
     *
404
     * @return ArgsFormat The inherited format.
0 ignored issues
show
Documentation introduced by
Should the return type not be ArgsFormat|null?

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...
405
     *
406
     * @see CommandConfig::buildArgsFormat()
407
     */
408 229
    private function getBaseFormat()
409
    {
410 229
        if ($this->parentCommand) {
411 31
            return $this->parentCommand->getArgsFormat();
412
        }
413
414 229
        if ($this->application) {
415 130
            return $this->application->getGlobalArgsFormat();
416
        }
417
418 99
        return null;
419
    }
420
421
    /**
422
     * Adds a sub-command.
423
     *
424
     * @param SubCommandConfig $config The sub-command configuration.
425
     *
426
     * @throws CannotAddCommandException If the command cannot be added.
427
     */
428 32
    private function addSubCommand(SubCommandConfig $config)
429
    {
430 32
        if (!$config->isEnabled()) {
431 1
            return;
432
        }
433
434 32
        $this->validateSubCommandName($config);
435
436 29
        $command = new self($config, $this->application, $this);
437
438 29
        $this->subCommands->add($command);
439
440 29
        if ($config->isDefault()) {
441 10
            $this->defaultSubCommands->add($command);
442
        }
443
444 29
        if (!$config->isAnonymous()) {
445 28
            $this->namedSubCommands->add($command);
446
        }
447 29
    }
448
449 50
    private function warnIfProcessTitleNotSupported($processTitle, IO $io)
450
    {
451 50
        if ($processTitle && !ProcessTitle::isSupported()) {
452
            $io->errorLine(
453
                '<warn>Notice: Install the proctitle PECL to be able to change the process title.</warn>',
454
                IO::VERY_VERBOSE
455
            );
456
        }
457 50
    }
458
459 32
    private function validateSubCommandName(SubCommandConfig $config)
460
    {
461 32
        $name = $config->getName();
462
463 32
        if (!$name) {
464 1
            throw CannotAddCommandException::nameEmpty();
465
        }
466
467 31
        if ($this->subCommands->contains($name)) {
468 4
            throw CannotAddCommandException::nameExists($name);
469
        }
470
471 31
        if ($config instanceof OptionCommandConfig) {
472 14
            if ($this->argsFormat->hasOption($name)) {
473 1
                throw CannotAddCommandException::optionExists($name);
474
            }
475
476 13
            if ($shortName = $config->getShortName()) {
477 10
                if ($this->subCommands->contains($shortName)) {
478 1
                    throw CannotAddCommandException::nameExists($name);
479
                }
480
481 10
                if ($this->argsFormat->hasOption($shortName)) {
482 1
                    throw CannotAddCommandException::optionExists($shortName);
483
                }
484
            }
485
        }
486 29
    }
487
488 50
    private function doHandle(Args $args, IO $io)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
489
    {
490 50 View Code Duplication
        if ($this->dispatcher && $this->dispatcher->hasListeners(ConsoleEvents::PRE_HANDLE)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
491 32
            $event = new PreHandleEvent($args, $io, $this);
492 32
            $this->dispatcher->dispatch(ConsoleEvents::PRE_HANDLE, $event);
493
494 32
            if ($event->isHandled()) {
495 5
                return $event->getStatusCode();
496
            }
497
        }
498
499 45
        $commandHandler = $this->config->getHandler();
500 45
        $handlerMethod = $this->config->getHandlerMethod();
501
502 45
        return $commandHandler->$handlerMethod($args, $io, $this);
503
    }
504
}
505