Completed
Push — develop ( f11ef2...d41b65 )
by David
06:01 queued 11s
created

Application::find()   F

Complexity

Conditions 22
Paths 340

Size

Total Lines 102

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 22
nc 340
nop 1
dl 0
loc 102
rs 1.2666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[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 Symfony\Component\Console;
13
14
use Symfony\Component\Console\Command\Command;
15
use Symfony\Component\Console\Command\HelpCommand;
16
use Symfony\Component\Console\Command\ListCommand;
17
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
18
use Symfony\Component\Console\Event\ConsoleCommandEvent;
19
use Symfony\Component\Console\Event\ConsoleErrorEvent;
20
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
21
use Symfony\Component\Console\Exception\CommandNotFoundException;
22
use Symfony\Component\Console\Exception\ExceptionInterface;
23
use Symfony\Component\Console\Exception\LogicException;
24
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
25
use Symfony\Component\Console\Formatter\OutputFormatter;
26
use Symfony\Component\Console\Helper\DebugFormatterHelper;
27
use Symfony\Component\Console\Helper\FormatterHelper;
28
use Symfony\Component\Console\Helper\Helper;
29
use Symfony\Component\Console\Helper\HelperSet;
30
use Symfony\Component\Console\Helper\ProcessHelper;
31
use Symfony\Component\Console\Helper\QuestionHelper;
32
use Symfony\Component\Console\Input\ArgvInput;
33
use Symfony\Component\Console\Input\ArrayInput;
34
use Symfony\Component\Console\Input\InputArgument;
35
use Symfony\Component\Console\Input\InputAwareInterface;
36
use Symfony\Component\Console\Input\InputDefinition;
37
use Symfony\Component\Console\Input\InputInterface;
38
use Symfony\Component\Console\Input\InputOption;
39
use Symfony\Component\Console\Output\ConsoleOutput;
40
use Symfony\Component\Console\Output\ConsoleOutputInterface;
41
use Symfony\Component\Console\Output\OutputInterface;
42
use Symfony\Component\Console\Style\SymfonyStyle;
43
use Symfony\Component\ErrorHandler\ErrorHandler;
44
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
45
use Symfony\Contracts\Service\ResetInterface;
46
47
/**
48
 * An Application is the container for a collection of commands.
49
 *
50
 * It is the main entry point of a Console application.
51
 *
52
 * This class is optimized for a standard CLI environment.
53
 *
54
 * Usage:
55
 *
56
 *     $app = new Application('myapp', '1.0 (stable)');
57
 *     $app->add(new SimpleCommand());
58
 *     $app->run();
59
 *
60
 * @author Fabien Potencier <[email protected]>
61
 */
62
class Application implements ResetInterface
63
{
64
    private $commands = [];
65
    private $wantHelps = false;
66
    private $runningCommand;
67
    private $name;
68
    private $version;
69
    private $commandLoader;
70
    private $catchExceptions = true;
71
    private $autoExit = true;
72
    private $definition;
73
    private $helperSet;
74
    private $dispatcher;
75
    private $terminal;
76
    private $defaultCommand;
77
    private $singleCommand = false;
78
    private $initialized;
79
80
    public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
81
    {
82
        $this->name = $name;
83
        $this->version = $version;
84
        $this->terminal = new Terminal();
85
        $this->defaultCommand = 'list';
86
    }
87
88
    /**
89
     * @final
90
     */
91
    public function setDispatcher(EventDispatcherInterface $dispatcher)
92
    {
93
        $this->dispatcher = $dispatcher;
94
    }
95
96
    public function setCommandLoader(CommandLoaderInterface $commandLoader)
97
    {
98
        $this->commandLoader = $commandLoader;
99
    }
100
101
    /**
102
     * Runs the current application.
103
     *
104
     * @return int 0 if everything went fine, or an error code
105
     *
106
     * @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}.
107
     */
108
    public function run(InputInterface $input = null, OutputInterface $output = null)
109
    {
110
        putenv('LINES='.$this->terminal->getHeight());
111
        putenv('COLUMNS='.$this->terminal->getWidth());
112
113
        if (null === $input) {
114
            $input = new ArgvInput();
115
        }
116
117
        if (null === $output) {
118
            $output = new ConsoleOutput();
119
        }
120
121
        $renderException = function (\Throwable $e) use ($output) {
122
            if ($output instanceof ConsoleOutputInterface) {
123
                $this->renderThrowable($e, $output->getErrorOutput());
124
            } else {
125
                $this->renderThrowable($e, $output);
126
            }
127
        };
128
        if ($phpHandler = set_exception_handler($renderException)) {
129
            restore_exception_handler();
130
            if (!\is_array($phpHandler) || !$phpHandler[0] instanceof ErrorHandler) {
0 ignored issues
show
Bug introduced by
The class Symfony\Component\ErrorHandler\ErrorHandler does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
131
                $errorHandler = true;
132
            } elseif ($errorHandler = $phpHandler[0]->setExceptionHandler($renderException)) {
133
                $phpHandler[0]->setExceptionHandler($errorHandler);
134
            }
135
        }
136
137
        $this->configureIO($input, $output);
138
139
        try {
140
            $exitCode = $this->doRun($input, $output);
141
        } catch (\Exception $e) {
142
            if (!$this->catchExceptions) {
143
                throw $e;
144
            }
145
146
            $renderException($e);
147
148
            $exitCode = $e->getCode();
149
            if (is_numeric($exitCode)) {
150
                $exitCode = (int) $exitCode;
151
                if (0 === $exitCode) {
152
                    $exitCode = 1;
153
                }
154
            } else {
155
                $exitCode = 1;
156
            }
157
        } finally {
158
            // if the exception handler changed, keep it
159
            // otherwise, unregister $renderException
160
            if (!$phpHandler) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $phpHandler of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
161
                if (set_exception_handler($renderException) === $renderException) {
162
                    restore_exception_handler();
163
                }
164
                restore_exception_handler();
165
            } elseif (!$errorHandler) {
0 ignored issues
show
Bug introduced by
The variable $errorHandler does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
166
                $finalHandler = $phpHandler[0]->setExceptionHandler(null);
167
                if ($finalHandler !== $renderException) {
168
                    $phpHandler[0]->setExceptionHandler($finalHandler);
169
                }
170
            }
171
        }
172
173
        if ($this->autoExit) {
174
            if ($exitCode > 255) {
175
                $exitCode = 255;
176
            }
177
178
            exit($exitCode);
179
        }
180
181
        return $exitCode;
182
    }
183
184
    /**
185
     * Runs the current application.
186
     *
187
     * @return int 0 if everything went fine, or an error code
188
     */
189
    public function doRun(InputInterface $input, OutputInterface $output)
190
    {
191
        if (true === $input->hasParameterOption(['--version', '-V'], true)) {
192
            $output->writeln($this->getLongVersion());
193
194
            return 0;
195
        }
196
197
        try {
198
            // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument.
199
            $input->bind($this->getDefinition());
200
        } catch (ExceptionInterface $e) {
201
            // Errors must be ignored, full binding/validation happens later when the command is known.
202
        }
203
204
        $name = $this->getCommandName($input);
205
        if (true === $input->hasParameterOption(['--help', '-h'], true)) {
206
            if (!$name) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $name of type string|null is loosely compared to false; 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...
207
                $name = 'help';
208
                $input = new ArrayInput(['command_name' => $this->defaultCommand]);
209
            } else {
210
                $this->wantHelps = true;
211
            }
212
        }
213
214
        if (!$name) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $name of type string|null is loosely compared to false; 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...
215
            $name = $this->defaultCommand;
216
            $definition = $this->getDefinition();
217
            $definition->setArguments(array_merge(
218
                $definition->getArguments(),
219
                [
220
                    'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name),
221
                ]
222
            ));
223
        }
224
225
        try {
226
            $this->runningCommand = null;
227
            // the command name MUST be the first element of the input
228
            $command = $this->find($name);
229
        } catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
230
            if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) || 1 !== \count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) {
231
                if (null !== $this->dispatcher) {
232
                    $event = new ConsoleErrorEvent($input, $output, $e);
233
                    $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
234
235
                    if (0 === $event->getExitCode()) {
236
                        return 0;
237
                    }
238
239
                    $e = $event->getError();
240
                }
241
242
                throw $e;
243
            }
244
245
            $alternative = $alternatives[0];
246
247
            $style = new SymfonyStyle($input, $output);
248
            $style->block(sprintf("\nCommand \"%s\" is not defined.\n", $name), null, 'error');
249
            if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) {
250
                if (null !== $this->dispatcher) {
251
                    $event = new ConsoleErrorEvent($input, $output, $e);
252
                    $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
253
254
                    return $event->getExitCode();
255
                }
256
257
                return 1;
258
            }
259
260
            $command = $this->find($alternative);
261
        }
262
263
        $this->runningCommand = $command;
264
        $exitCode = $this->doRunCommand($command, $input, $output);
265
        $this->runningCommand = null;
266
267
        return $exitCode;
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     */
273
    public function reset()
274
    {
275
    }
276
277
    public function setHelperSet(HelperSet $helperSet)
278
    {
279
        $this->helperSet = $helperSet;
280
    }
281
282
    /**
283
     * Get the helper set associated with the command.
284
     *
285
     * @return HelperSet The HelperSet instance associated with this command
286
     */
287
    public function getHelperSet()
288
    {
289
        if (!$this->helperSet) {
290
            $this->helperSet = $this->getDefaultHelperSet();
291
        }
292
293
        return $this->helperSet;
294
    }
295
296
    public function setDefinition(InputDefinition $definition)
297
    {
298
        $this->definition = $definition;
299
    }
300
301
    /**
302
     * Gets the InputDefinition related to this Application.
303
     *
304
     * @return InputDefinition The InputDefinition instance
305
     */
306
    public function getDefinition()
307
    {
308
        if (!$this->definition) {
309
            $this->definition = $this->getDefaultInputDefinition();
310
        }
311
312
        if ($this->singleCommand) {
313
            $inputDefinition = $this->definition;
314
            $inputDefinition->setArguments();
315
316
            return $inputDefinition;
317
        }
318
319
        return $this->definition;
320
    }
321
322
    /**
323
     * Gets the help message.
324
     *
325
     * @return string A help message
326
     */
327
    public function getHelp()
328
    {
329
        return $this->getLongVersion();
330
    }
331
332
    /**
333
     * Gets whether to catch exceptions or not during commands execution.
334
     *
335
     * @return bool Whether to catch exceptions or not during commands execution
336
     */
337
    public function areExceptionsCaught()
338
    {
339
        return $this->catchExceptions;
340
    }
341
342
    /**
343
     * Sets whether to catch exceptions or not during commands execution.
344
     */
345
    public function setCatchExceptions(bool $boolean)
346
    {
347
        $this->catchExceptions = $boolean;
348
    }
349
350
    /**
351
     * Gets whether to automatically exit after a command execution or not.
352
     *
353
     * @return bool Whether to automatically exit after a command execution or not
354
     */
355
    public function isAutoExitEnabled()
356
    {
357
        return $this->autoExit;
358
    }
359
360
    /**
361
     * Sets whether to automatically exit after a command execution or not.
362
     */
363
    public function setAutoExit(bool $boolean)
364
    {
365
        $this->autoExit = $boolean;
366
    }
367
368
    /**
369
     * Gets the name of the application.
370
     *
371
     * @return string The application name
372
     */
373
    public function getName()
374
    {
375
        return $this->name;
376
    }
377
378
    /**
379
     * Sets the application name.
380
     **/
381
    public function setName(string $name)
382
    {
383
        $this->name = $name;
384
    }
385
386
    /**
387
     * Gets the application version.
388
     *
389
     * @return string The application version
390
     */
391
    public function getVersion()
392
    {
393
        return $this->version;
394
    }
395
396
    /**
397
     * Sets the application version.
398
     */
399
    public function setVersion(string $version)
400
    {
401
        $this->version = $version;
402
    }
403
404
    /**
405
     * Returns the long version of the application.
406
     *
407
     * @return string The long application version
408
     */
409
    public function getLongVersion()
410
    {
411
        if ('UNKNOWN' !== $this->getName()) {
412
            if ('UNKNOWN' !== $this->getVersion()) {
413
                return sprintf('%s <info>%s</info>', $this->getName(), $this->getVersion());
414
            }
415
416
            return $this->getName();
417
        }
418
419
        return 'Console Tool';
420
    }
421
422
    /**
423
     * Registers a new command.
424
     *
425
     * @return Command The newly created command
426
     */
427
    public function register(string $name)
428
    {
429
        return $this->add(new Command($name));
430
    }
431
432
    /**
433
     * Adds an array of command objects.
434
     *
435
     * If a Command is not enabled it will not be added.
436
     *
437
     * @param Command[] $commands An array of commands
438
     */
439
    public function addCommands(array $commands)
440
    {
441
        foreach ($commands as $command) {
442
            $this->add($command);
443
        }
444
    }
445
446
    /**
447
     * Adds a command object.
448
     *
449
     * If a command with the same name already exists, it will be overridden.
450
     * If the command is not enabled it will not be added.
451
     *
452
     * @return Command|null The registered command if enabled or null
453
     */
454
    public function add(Command $command)
455
    {
456
        $this->init();
457
458
        $command->setApplication($this);
459
460
        if (!$command->isEnabled()) {
461
            $command->setApplication(null);
462
463
            return null;
464
        }
465
466
        // Will throw if the command is not correctly initialized.
467
        $command->getDefinition();
468
469
        if (!$command->getName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $command->getName() of type string|null is loosely compared to false; 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...
470
            throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command)));
471
        }
472
473
        $this->commands[$command->getName()] = $command;
474
475
        foreach ($command->getAliases() as $alias) {
476
            $this->commands[$alias] = $command;
477
        }
478
479
        return $command;
480
    }
481
482
    /**
483
     * Returns a registered command by name or alias.
484
     *
485
     * @return Command A Command object
486
     *
487
     * @throws CommandNotFoundException When given command name does not exist
488
     */
489
    public function get(string $name)
490
    {
491
        $this->init();
492
493
        if (!$this->has($name)) {
494
            throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
495
        }
496
497
        // When the command has a different name than the one used at the command loader level
498
        if (!isset($this->commands[$name])) {
499
            throw new CommandNotFoundException(sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name));
500
        }
501
502
        $command = $this->commands[$name];
503
504
        if ($this->wantHelps) {
505
            $this->wantHelps = false;
506
507
            $helpCommand = $this->get('help');
508
            $helpCommand->setCommand($command);
509
510
            return $helpCommand;
511
        }
512
513
        return $command;
514
    }
515
516
    /**
517
     * Returns true if the command exists, false otherwise.
518
     *
519
     * @return bool true if the command exists, false otherwise
520
     */
521
    public function has(string $name)
522
    {
523
        $this->init();
524
525
        return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name) && $this->add($this->commandLoader->get($name)));
526
    }
527
528
    /**
529
     * Returns an array of all unique namespaces used by currently registered commands.
530
     *
531
     * It does not return the global namespace which always exists.
532
     *
533
     * @return string[] An array of namespaces
534
     */
535
    public function getNamespaces()
536
    {
537
        $namespaces = [];
538
        foreach ($this->all() as $command) {
539
            if ($command->isHidden()) {
540
                continue;
541
            }
542
543
            $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName()));
544
545
            foreach ($command->getAliases() as $alias) {
546
                $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias));
547
            }
548
        }
549
550
        return array_values(array_unique(array_filter($namespaces)));
551
    }
552
553
    /**
554
     * Finds a registered namespace by a name or an abbreviation.
555
     *
556
     * @return string A registered namespace
557
     *
558
     * @throws NamespaceNotFoundException When namespace is incorrect or ambiguous
559
     */
560
    public function findNamespace(string $namespace)
561
    {
562
        $allNamespaces = $this->getNamespaces();
563
        $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $namespace);
564
        $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces);
565
566
        if (empty($namespaces)) {
567
            $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
568
569
            if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
570
                if (1 == \count($alternatives)) {
571
                    $message .= "\n\nDid you mean this?\n    ";
572
                } else {
573
                    $message .= "\n\nDid you mean one of these?\n    ";
574
                }
575
576
                $message .= implode("\n    ", $alternatives);
577
            }
578
579
            throw new NamespaceNotFoundException($message, $alternatives);
580
        }
581
582
        $exact = \in_array($namespace, $namespaces, true);
583
        if (\count($namespaces) > 1 && !$exact) {
584
            throw new NamespaceNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces));
585
        }
586
587
        return $exact ? $namespace : reset($namespaces);
588
    }
589
590
    /**
591
     * Finds a command by name or alias.
592
     *
593
     * Contrary to get, this command tries to find the best
594
     * match if you give it an abbreviation of a name or alias.
595
     *
596
     * @return Command A Command instance
597
     *
598
     * @throws CommandNotFoundException When command name is incorrect or ambiguous
599
     */
600
    public function find(string $name)
601
    {
602
        $this->init();
603
604
        $aliases = [];
605
606
        foreach ($this->commands as $command) {
607
            foreach ($command->getAliases() as $alias) {
608
                if (!$this->has($alias)) {
609
                    $this->commands[$alias] = $command;
610
                }
611
            }
612
        }
613
614
        if ($this->has($name)) {
615
            return $this->get($name);
616
        }
617
618
        $allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands);
619
        $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name);
620
        $commands = preg_grep('{^'.$expr.'}', $allCommands);
621
622
        if (empty($commands)) {
623
            $commands = preg_grep('{^'.$expr.'}i', $allCommands);
624
        }
625
626
        // if no commands matched or we just matched namespaces
627
        if (empty($commands) || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) {
628
            if (false !== $pos = strrpos($name, ':')) {
629
                // check if a namespace exists and contains commands
630
                $this->findNamespace(substr($name, 0, $pos));
631
            }
632
633
            $message = sprintf('Command "%s" is not defined.', $name);
634
635
            if ($alternatives = $this->findAlternatives($name, $allCommands)) {
636
                // remove hidden commands
637
                $alternatives = array_filter($alternatives, function ($name) {
638
                    return !$this->get($name)->isHidden();
639
                });
640
641
                if (1 == \count($alternatives)) {
642
                    $message .= "\n\nDid you mean this?\n    ";
643
                } else {
644
                    $message .= "\n\nDid you mean one of these?\n    ";
645
                }
646
                $message .= implode("\n    ", $alternatives);
647
            }
648
649
            throw new CommandNotFoundException($message, array_values($alternatives));
650
        }
651
652
        // filter out aliases for commands which are already on the list
653
        if (\count($commands) > 1) {
654
            $commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands;
655
            $commands = array_unique(array_filter($commands, function ($nameOrAlias) use (&$commandList, $commands, &$aliases) {
656
                if (!$commandList[$nameOrAlias] instanceof Command) {
657
                    $commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias);
658
                }
659
660
                $commandName = $commandList[$nameOrAlias]->getName();
661
662
                $aliases[$nameOrAlias] = $commandName;
663
664
                return $commandName === $nameOrAlias || !\in_array($commandName, $commands);
665
            }));
666
        }
667
668
        if (\count($commands) > 1) {
669
            $usableWidth = $this->terminal->getWidth() - 10;
670
            $abbrevs = array_values($commands);
671
            $maxLen = 0;
672
            foreach ($abbrevs as $abbrev) {
673
                $maxLen = max(Helper::strlen($abbrev), $maxLen);
674
            }
675
            $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) {
0 ignored issues
show
Bug introduced by
The variable $commandList does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
676
                if ($commandList[$cmd]->isHidden()) {
677
                    unset($commands[array_search($cmd, $commands)]);
678
679
                    return false;
680
                }
681
682
                $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription();
683
684
                return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
685
            }, array_values($commands));
686
687
            if (\count($commands) > 1) {
688
                $suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs));
689
690
                throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands));
691
            }
692
        }
693
694
        $command = $this->get(reset($commands));
695
696
        if ($command->isHidden()) {
697
            throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
698
        }
699
700
        return $command;
701
    }
702
703
    /**
704
     * Gets the commands (registered in the given namespace if provided).
705
     *
706
     * The array keys are the full names and the values the command instances.
707
     *
708
     * @return Command[] An array of Command instances
709
     */
710
    public function all(string $namespace = null)
711
    {
712
        $this->init();
713
714
        if (null === $namespace) {
715
            if (!$this->commandLoader) {
716
                return $this->commands;
717
            }
718
719
            $commands = $this->commands;
720
            foreach ($this->commandLoader->getNames() as $name) {
721
                if (!isset($commands[$name]) && $this->has($name)) {
722
                    $commands[$name] = $this->get($name);
723
                }
724
            }
725
726
            return $commands;
727
        }
728
729
        $commands = [];
730
        foreach ($this->commands as $name => $command) {
731
            if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) {
732
                $commands[$name] = $command;
733
            }
734
        }
735
736
        if ($this->commandLoader) {
737
            foreach ($this->commandLoader->getNames() as $name) {
738
                if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1) && $this->has($name)) {
739
                    $commands[$name] = $this->get($name);
740
                }
741
            }
742
        }
743
744
        return $commands;
745
    }
746
747
    /**
748
     * Returns an array of possible abbreviations given a set of names.
749
     *
750
     * @return string[][] An array of abbreviations
751
     */
752
    public static function getAbbreviations(array $names)
753
    {
754
        $abbrevs = [];
755
        foreach ($names as $name) {
756
            for ($len = \strlen($name); $len > 0; --$len) {
757
                $abbrev = substr($name, 0, $len);
758
                $abbrevs[$abbrev][] = $name;
759
            }
760
        }
761
762
        return $abbrevs;
763
    }
764
765
    public function renderThrowable(\Throwable $e, OutputInterface $output): void
766
    {
767
        $output->writeln('', OutputInterface::VERBOSITY_QUIET);
768
769
        $this->doRenderThrowable($e, $output);
770
771
        if (null !== $this->runningCommand) {
772
            $output->writeln(sprintf('<info>%s</info>', sprintf($this->runningCommand->getSynopsis(), $this->getName())), OutputInterface::VERBOSITY_QUIET);
0 ignored issues
show
Bug introduced by
The method getSynopsis cannot be called on $this->runningCommand (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
773
            $output->writeln('', OutputInterface::VERBOSITY_QUIET);
774
        }
775
    }
776
777
    protected function doRenderThrowable(\Throwable $e, OutputInterface $output): void
778
    {
779
        do {
780
            $message = trim($e->getMessage());
781
            if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
782
                $class = get_debug_type($e);
783
                $title = sprintf('  [%s%s]  ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
784
                $len = Helper::strlen($title);
785
            } else {
786
                $len = 0;
787
            }
788
789
            if (false !== strpos($message, "@anonymous\0")) {
790
                $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
791
                    return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
792
                }, $message);
793
            }
794
795
            $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : PHP_INT_MAX;
796
            $lines = [];
797
            foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) {
798
                foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
799
                    // pre-format lines to get the right string length
800
                    $lineLength = Helper::strlen($line) + 4;
801
                    $lines[] = [$line, $lineLength];
802
803
                    $len = max($lineLength, $len);
804
                }
805
            }
806
807
            $messages = [];
808
            if (!$e instanceof ExceptionInterface || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
809
                $messages[] = sprintf('<comment>%s</comment>', OutputFormatter::escape(sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a')));
810
            }
811
            $messages[] = $emptyLine = sprintf('<error>%s</error>', str_repeat(' ', $len));
812
            if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
813
                $messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - Helper::strlen($title))));
0 ignored issues
show
Bug introduced by
The variable $title does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
814
            }
815
            foreach ($lines as $line) {
816
                $messages[] = sprintf('<error>  %s  %s</error>', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1]));
817
            }
818
            $messages[] = $emptyLine;
819
            $messages[] = '';
820
821
            $output->writeln($messages, OutputInterface::VERBOSITY_QUIET);
0 ignored issues
show
Documentation introduced by
$messages is of type array<integer,string>, but the function expects a string|object<Symfony\Co...onsole\Output\iterable>.

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...
822
823
            if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
824
                $output->writeln('<comment>Exception trace:</comment>', OutputInterface::VERBOSITY_QUIET);
825
826
                // exception related properties
827
                $trace = $e->getTrace();
828
829
                array_unshift($trace, [
830
                    'function' => '',
831
                    'file' => $e->getFile() ?: 'n/a',
832
                    'line' => $e->getLine() ?: 'n/a',
833
                    'args' => [],
834
                ]);
835
836
                for ($i = 0, $count = \count($trace); $i < $count; ++$i) {
837
                    $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
838
                    $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
839
                    $function = isset($trace[$i]['function']) ? $trace[$i]['function'] : '';
840
                    $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
841
                    $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
842
843
                    $output->writeln(sprintf(' %s%s at <info>%s:%s</info>', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET);
844
                }
845
846
                $output->writeln('', OutputInterface::VERBOSITY_QUIET);
847
            }
848
        } while ($e = $e->getPrevious());
849
    }
850
851
    /**
852
     * Configures the input and output instances based on the user arguments and options.
853
     */
854
    protected function configureIO(InputInterface $input, OutputInterface $output)
855
    {
856
        if (true === $input->hasParameterOption(['--ansi'], true)) {
857
            $output->setDecorated(true);
858
        } elseif (true === $input->hasParameterOption(['--no-ansi'], true)) {
859
            $output->setDecorated(false);
860
        }
861
862
        if (true === $input->hasParameterOption(['--no-interaction', '-n'], true)) {
863
            $input->setInteractive(false);
864
        }
865
866
        switch ($shellVerbosity = (int) getenv('SHELL_VERBOSITY')) {
867
            case -1: $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); break;
868
            case 1: $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); break;
869
            case 2: $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); break;
870
            case 3: $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); break;
871
            default: $shellVerbosity = 0; break;
872
        }
873
874
        if (true === $input->hasParameterOption(['--quiet', '-q'], true)) {
875
            $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
876
            $shellVerbosity = -1;
877
        } else {
878
            if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) {
879
                $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
880
                $shellVerbosity = 3;
881
            } elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) {
882
                $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE);
883
                $shellVerbosity = 2;
884
            } elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) {
885
                $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
886
                $shellVerbosity = 1;
887
            }
888
        }
889
890
        if (-1 === $shellVerbosity) {
891
            $input->setInteractive(false);
892
        }
893
894
        putenv('SHELL_VERBOSITY='.$shellVerbosity);
895
        $_ENV['SHELL_VERBOSITY'] = $shellVerbosity;
896
        $_SERVER['SHELL_VERBOSITY'] = $shellVerbosity;
897
    }
898
899
    /**
900
     * Runs the current command.
901
     *
902
     * If an event dispatcher has been attached to the application,
903
     * events are also dispatched during the life-cycle of the command.
904
     *
905
     * @return int 0 if everything went fine, or an error code
906
     */
907
    protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
908
    {
909
        foreach ($command->getHelperSet() as $helper) {
0 ignored issues
show
Bug introduced by
The expression $command->getHelperSet() of type null|object<Symfony\Comp...nsole\Helper\HelperSet> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
910
            if ($helper instanceof InputAwareInterface) {
911
                $helper->setInput($input);
912
            }
913
        }
914
915
        if (null === $this->dispatcher) {
916
            return $command->run($input, $output);
917
        }
918
919
        // bind before the console.command event, so the listeners have access to input options/arguments
920
        try {
921
            $command->mergeApplicationDefinition();
922
            $input->bind($command->getDefinition());
923
        } catch (ExceptionInterface $e) {
924
            // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
925
        }
926
927
        $event = new ConsoleCommandEvent($command, $input, $output);
928
        $e = null;
929
930
        try {
931
            $this->dispatcher->dispatch($event, ConsoleEvents::COMMAND);
932
933
            if ($event->commandShouldRun()) {
934
                $exitCode = $command->run($input, $output);
935
            } else {
936
                $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED;
937
            }
938
        } catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
939
            $event = new ConsoleErrorEvent($input, $output, $e, $command);
940
            $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
941
            $e = $event->getError();
942
943
            if (0 === $exitCode = $event->getExitCode()) {
944
                $e = null;
945
            }
946
        }
947
948
        $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode);
949
        $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE);
950
951
        if (null !== $e) {
952
            throw $e;
953
        }
954
955
        return $event->getExitCode();
956
    }
957
958
    /**
959
     * Gets the name of the command based on input.
960
     *
961
     * @return string|null
962
     */
963
    protected function getCommandName(InputInterface $input)
964
    {
965
        return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument();
966
    }
967
968
    /**
969
     * Gets the default input definition.
970
     *
971
     * @return InputDefinition An InputDefinition instance
972
     */
973
    protected function getDefaultInputDefinition()
974
    {
975
        return new InputDefinition([
976
            new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
977
978
            new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
979
            new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
980
            new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'),
981
            new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'),
982
            new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
983
            new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
984
            new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
985
        ]);
986
    }
987
988
    /**
989
     * Gets the default commands that should always be available.
990
     *
991
     * @return Command[] An array of default Command instances
992
     */
993
    protected function getDefaultCommands()
994
    {
995
        return [new HelpCommand(), new ListCommand()];
996
    }
997
998
    /**
999
     * Gets the default helper set with the helpers that should always be available.
1000
     *
1001
     * @return HelperSet A HelperSet instance
1002
     */
1003
    protected function getDefaultHelperSet()
1004
    {
1005
        return new HelperSet([
1006
            new FormatterHelper(),
1007
            new DebugFormatterHelper(),
1008
            new ProcessHelper(),
1009
            new QuestionHelper(),
1010
        ]);
1011
    }
1012
1013
    /**
1014
     * Returns abbreviated suggestions in string format.
1015
     */
1016
    private function getAbbreviationSuggestions(array $abbrevs): string
1017
    {
1018
        return '    '.implode("\n    ", $abbrevs);
1019
    }
1020
1021
    /**
1022
     * Returns the namespace part of the command name.
1023
     *
1024
     * This method is not part of public API and should not be used directly.
1025
     *
1026
     * @return string The namespace of the command
1027
     */
1028
    public function extractNamespace(string $name, int $limit = null)
1029
    {
1030
        $parts = explode(':', $name, -1);
1031
1032
        return implode(':', null === $limit ? $parts : \array_slice($parts, 0, $limit));
1033
    }
1034
1035
    /**
1036
     * Finds alternative of $name among $collection,
1037
     * if nothing is found in $collection, try in $abbrevs.
1038
     *
1039
     * @return string[] A sorted array of similar string
1040
     */
1041
    private function findAlternatives(string $name, iterable $collection): array
1042
    {
1043
        $threshold = 1e3;
1044
        $alternatives = [];
1045
1046
        $collectionParts = [];
1047
        foreach ($collection as $item) {
1048
            $collectionParts[$item] = explode(':', $item);
1049
        }
1050
1051
        foreach (explode(':', $name) as $i => $subname) {
1052
            foreach ($collectionParts as $collectionName => $parts) {
1053
                $exists = isset($alternatives[$collectionName]);
1054
                if (!isset($parts[$i]) && $exists) {
1055
                    $alternatives[$collectionName] += $threshold;
1056
                    continue;
1057
                } elseif (!isset($parts[$i])) {
1058
                    continue;
1059
                }
1060
1061
                $lev = levenshtein($subname, $parts[$i]);
1062
                if ($lev <= \strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) {
1063
                    $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
1064
                } elseif ($exists) {
1065
                    $alternatives[$collectionName] += $threshold;
1066
                }
1067
            }
1068
        }
1069
1070
        foreach ($collection as $item) {
1071
            $lev = levenshtein($name, $item);
1072
            if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) {
1073
                $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
1074
            }
1075
        }
1076
1077
        $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
1078
        ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
1079
1080
        return array_keys($alternatives);
1081
    }
1082
1083
    /**
1084
     * Sets the default Command name.
1085
     *
1086
     * @return self
1087
     */
1088
    public function setDefaultCommand(string $commandName, bool $isSingleCommand = false)
1089
    {
1090
        $this->defaultCommand = $commandName;
1091
1092
        if ($isSingleCommand) {
1093
            // Ensure the command exist
1094
            $this->find($commandName);
1095
1096
            $this->singleCommand = true;
1097
        }
1098
1099
        return $this;
1100
    }
1101
1102
    /**
1103
     * @internal
1104
     */
1105
    public function isSingleCommand(): bool
1106
    {
1107
        return $this->singleCommand;
1108
    }
1109
1110
    private function splitStringByWidth(string $string, int $width): array
1111
    {
1112
        // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly.
1113
        // additionally, array_slice() is not enough as some character has doubled width.
1114
        // we need a function to split string not by character count but by string width
1115
        if (false === $encoding = mb_detect_encoding($string, null, true)) {
1116
            return str_split($string, $width);
1117
        }
1118
1119
        $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
1120
        $lines = [];
1121
        $line = '';
1122
1123
        $offset = 0;
1124
        while (preg_match('/.{1,10000}/u', $utf8String, $m, 0, $offset)) {
1125
            $offset += \strlen($m[0]);
1126
1127
            foreach (preg_split('//u', $m[0]) as $char) {
1128
                // test if $char could be appended to current line
1129
                if (mb_strwidth($line.$char, 'utf8') <= $width) {
1130
                    $line .= $char;
1131
                    continue;
1132
                }
1133
                // if not, push current line to array and make new line
1134
                $lines[] = str_pad($line, $width);
1135
                $line = $char;
1136
            }
1137
        }
1138
1139
        $lines[] = \count($lines) ? str_pad($line, $width) : $line;
1140
1141
        mb_convert_variables($encoding, 'utf8', $lines);
1142
1143
        return $lines;
1144
    }
1145
1146
    /**
1147
     * Returns all namespaces of the command name.
1148
     *
1149
     * @return string[] The namespaces of the command
1150
     */
1151
    private function extractAllNamespaces(string $name): array
1152
    {
1153
        // -1 as third argument is needed to skip the command short name when exploding
1154
        $parts = explode(':', $name, -1);
1155
        $namespaces = [];
1156
1157
        foreach ($parts as $part) {
1158
            if (\count($namespaces)) {
1159
                $namespaces[] = end($namespaces).':'.$part;
1160
            } else {
1161
                $namespaces[] = $part;
1162
            }
1163
        }
1164
1165
        return $namespaces;
1166
    }
1167
1168
    private function init()
1169
    {
1170
        if ($this->initialized) {
1171
            return;
1172
        }
1173
        $this->initialized = true;
1174
1175
        foreach ($this->getDefaultCommands() as $command) {
1176
            $this->add($command);
1177
        }
1178
    }
1179
}
1180