Passed
Push — develop ( 30cf64...589229 )
by Guillaume
06:18 queued 04:10
created

Application   F

Complexity

Total Complexity 219

Size/Duplication

Total Lines 1110
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 457
dl 0
loc 1110
rs 2
c 0
b 0
f 0
wmc 219

How to fix   Complexity   

Complex Class

Complex classes like Application 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.

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 Application, and based on these observations, apply Extract Interface, too.

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) {
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) {
161
                if (set_exception_handler($renderException) === $renderException) {
162
                    restore_exception_handler();
163
                }
164
                restore_exception_handler();
165
            } elseif (!$errorHandler) {
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) {
207
                $name = 'help';
208
                $input = new ArrayInput(['command_name' => $this->defaultCommand]);
209
            } else {
210
                $this->wantHelps = true;
211
            }
212
        }
213
214
        if (!$name) {
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) {
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()) {
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
        $command = $this->commands[$name];
498
499
        if ($this->wantHelps) {
500
            $this->wantHelps = false;
501
502
            $helpCommand = $this->get('help');
503
            $helpCommand->setCommand($command);
504
505
            return $helpCommand;
506
        }
507
508
        return $command;
509
    }
510
511
    /**
512
     * Returns true if the command exists, false otherwise.
513
     *
514
     * @return bool true if the command exists, false otherwise
515
     */
516
    public function has(string $name)
517
    {
518
        $this->init();
519
520
        return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name) && $this->add($this->commandLoader->get($name)));
521
    }
522
523
    /**
524
     * Returns an array of all unique namespaces used by currently registered commands.
525
     *
526
     * It does not return the global namespace which always exists.
527
     *
528
     * @return string[] An array of namespaces
529
     */
530
    public function getNamespaces()
531
    {
532
        $namespaces = [];
533
        foreach ($this->all() as $command) {
534
            if ($command->isHidden()) {
535
                continue;
536
            }
537
538
            $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName()));
539
540
            foreach ($command->getAliases() as $alias) {
541
                $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias));
542
            }
543
        }
544
545
        return array_values(array_unique(array_filter($namespaces)));
546
    }
547
548
    /**
549
     * Finds a registered namespace by a name or an abbreviation.
550
     *
551
     * @return string A registered namespace
552
     *
553
     * @throws NamespaceNotFoundException When namespace is incorrect or ambiguous
554
     */
555
    public function findNamespace(string $namespace)
556
    {
557
        $allNamespaces = $this->getNamespaces();
558
        $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $namespace);
559
        $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces);
560
561
        if (empty($namespaces)) {
562
            $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
563
564
            if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
565
                if (1 == \count($alternatives)) {
566
                    $message .= "\n\nDid you mean this?\n    ";
567
                } else {
568
                    $message .= "\n\nDid you mean one of these?\n    ";
569
                }
570
571
                $message .= implode("\n    ", $alternatives);
572
            }
573
574
            throw new NamespaceNotFoundException($message, $alternatives);
575
        }
576
577
        $exact = \in_array($namespace, $namespaces, true);
578
        if (\count($namespaces) > 1 && !$exact) {
579
            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));
580
        }
581
582
        return $exact ? $namespace : reset($namespaces);
583
    }
584
585
    /**
586
     * Finds a command by name or alias.
587
     *
588
     * Contrary to get, this command tries to find the best
589
     * match if you give it an abbreviation of a name or alias.
590
     *
591
     * @return Command A Command instance
592
     *
593
     * @throws CommandNotFoundException When command name is incorrect or ambiguous
594
     */
595
    public function find(string $name)
596
    {
597
        $this->init();
598
599
        $aliases = [];
600
601
        foreach ($this->commands as $command) {
602
            foreach ($command->getAliases() as $alias) {
603
                if (!$this->has($alias)) {
604
                    $this->commands[$alias] = $command;
605
                }
606
            }
607
        }
608
609
        if ($this->has($name)) {
610
            return $this->get($name);
611
        }
612
613
        $allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands);
614
        $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name);
615
        $commands = preg_grep('{^'.$expr.'}', $allCommands);
616
617
        if (empty($commands)) {
618
            $commands = preg_grep('{^'.$expr.'}i', $allCommands);
619
        }
620
621
        // if no commands matched or we just matched namespaces
622
        if (empty($commands) || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) {
623
            if (false !== $pos = strrpos($name, ':')) {
624
                // check if a namespace exists and contains commands
625
                $this->findNamespace(substr($name, 0, $pos));
626
            }
627
628
            $message = sprintf('Command "%s" is not defined.', $name);
629
630
            if ($alternatives = $this->findAlternatives($name, $allCommands)) {
631
                // remove hidden commands
632
                $alternatives = array_filter($alternatives, function ($name) {
633
                    return !$this->get($name)->isHidden();
634
                });
635
636
                if (1 == \count($alternatives)) {
637
                    $message .= "\n\nDid you mean this?\n    ";
638
                } else {
639
                    $message .= "\n\nDid you mean one of these?\n    ";
640
                }
641
                $message .= implode("\n    ", $alternatives);
642
            }
643
644
            throw new CommandNotFoundException($message, array_values($alternatives));
645
        }
646
647
        // filter out aliases for commands which are already on the list
648
        if (\count($commands) > 1) {
649
            $commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands;
650
            $commands = array_unique(array_filter($commands, function ($nameOrAlias) use (&$commandList, $commands, &$aliases) {
651
                if (!$commandList[$nameOrAlias] instanceof Command) {
652
                    $commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias);
653
                }
654
655
                $commandName = $commandList[$nameOrAlias]->getName();
656
657
                $aliases[$nameOrAlias] = $commandName;
658
659
                return $commandName === $nameOrAlias || !\in_array($commandName, $commands);
660
            }));
661
        }
662
663
        if (\count($commands) > 1) {
664
            $usableWidth = $this->terminal->getWidth() - 10;
665
            $abbrevs = array_values($commands);
666
            $maxLen = 0;
667
            foreach ($abbrevs as $abbrev) {
668
                $maxLen = max(Helper::strlen($abbrev), $maxLen);
669
            }
670
            $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) {
671
                if ($commandList[$cmd]->isHidden()) {
672
                    unset($commands[array_search($cmd, $commands)]);
673
674
                    return false;
675
                }
676
677
                $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription();
678
679
                return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
680
            }, array_values($commands));
681
682
            if (\count($commands) > 1) {
683
                $suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs));
684
685
                throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands));
686
            }
687
        }
688
689
        $command = $this->get(reset($commands));
690
691
        if ($command->isHidden()) {
692
            throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
693
        }
694
695
        return $command;
696
    }
697
698
    /**
699
     * Gets the commands (registered in the given namespace if provided).
700
     *
701
     * The array keys are the full names and the values the command instances.
702
     *
703
     * @return Command[] An array of Command instances
704
     */
705
    public function all(string $namespace = null)
706
    {
707
        $this->init();
708
709
        if (null === $namespace) {
710
            if (!$this->commandLoader) {
711
                return $this->commands;
712
            }
713
714
            $commands = $this->commands;
715
            foreach ($this->commandLoader->getNames() as $name) {
716
                if (!isset($commands[$name]) && $this->has($name)) {
717
                    $commands[$name] = $this->get($name);
718
                }
719
            }
720
721
            return $commands;
722
        }
723
724
        $commands = [];
725
        foreach ($this->commands as $name => $command) {
726
            if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) {
727
                $commands[$name] = $command;
728
            }
729
        }
730
731
        if ($this->commandLoader) {
732
            foreach ($this->commandLoader->getNames() as $name) {
733
                if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1) && $this->has($name)) {
734
                    $commands[$name] = $this->get($name);
735
                }
736
            }
737
        }
738
739
        return $commands;
740
    }
741
742
    /**
743
     * Returns an array of possible abbreviations given a set of names.
744
     *
745
     * @return string[][] An array of abbreviations
746
     */
747
    public static function getAbbreviations(array $names)
748
    {
749
        $abbrevs = [];
750
        foreach ($names as $name) {
751
            for ($len = \strlen($name); $len > 0; --$len) {
752
                $abbrev = substr($name, 0, $len);
753
                $abbrevs[$abbrev][] = $name;
754
            }
755
        }
756
757
        return $abbrevs;
758
    }
759
760
    public function renderThrowable(\Throwable $e, OutputInterface $output): void
761
    {
762
        $output->writeln('', OutputInterface::VERBOSITY_QUIET);
763
764
        $this->doRenderThrowable($e, $output);
765
766
        if (null !== $this->runningCommand) {
767
            $output->writeln(sprintf('<info>%s</info>', sprintf($this->runningCommand->getSynopsis(), $this->getName())), OutputInterface::VERBOSITY_QUIET);
768
            $output->writeln('', OutputInterface::VERBOSITY_QUIET);
769
        }
770
    }
771
772
    protected function doRenderThrowable(\Throwable $e, OutputInterface $output): void
773
    {
774
        do {
775
            $message = trim($e->getMessage());
776
            if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
777
                $class = get_debug_type($e);
778
                $title = sprintf('  [%s%s]  ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
779
                $len = Helper::strlen($title);
780
            } else {
781
                $len = 0;
782
            }
783
784
            if (false !== strpos($message, "@anonymous\0")) {
785
                $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) {
786
                    return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
787
                }, $message);
788
            }
789
790
            $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : PHP_INT_MAX;
791
            $lines = [];
792
            foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) {
793
                foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
794
                    // pre-format lines to get the right string length
795
                    $lineLength = Helper::strlen($line) + 4;
796
                    $lines[] = [$line, $lineLength];
797
798
                    $len = max($lineLength, $len);
799
                }
800
            }
801
802
            $messages = [];
803
            if (!$e instanceof ExceptionInterface || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
804
                $messages[] = sprintf('<comment>%s</comment>', OutputFormatter::escape(sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a')));
805
            }
806
            $messages[] = $emptyLine = sprintf('<error>%s</error>', str_repeat(' ', $len));
807
            if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
808
                $messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - Helper::strlen($title))));
809
            }
810
            foreach ($lines as $line) {
811
                $messages[] = sprintf('<error>  %s  %s</error>', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1]));
812
            }
813
            $messages[] = $emptyLine;
814
            $messages[] = '';
815
816
            $output->writeln($messages, OutputInterface::VERBOSITY_QUIET);
817
818
            if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
819
                $output->writeln('<comment>Exception trace:</comment>', OutputInterface::VERBOSITY_QUIET);
820
821
                // exception related properties
822
                $trace = $e->getTrace();
823
824
                array_unshift($trace, [
825
                    'function' => '',
826
                    'file' => $e->getFile() ?: 'n/a',
827
                    'line' => $e->getLine() ?: 'n/a',
828
                    'args' => [],
829
                ]);
830
831
                for ($i = 0, $count = \count($trace); $i < $count; ++$i) {
832
                    $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
833
                    $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
834
                    $function = isset($trace[$i]['function']) ? $trace[$i]['function'] : '';
835
                    $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
836
                    $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
837
838
                    $output->writeln(sprintf(' %s%s at <info>%s:%s</info>', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET);
839
                }
840
841
                $output->writeln('', OutputInterface::VERBOSITY_QUIET);
842
            }
843
        } while ($e = $e->getPrevious());
844
    }
845
846
    /**
847
     * Configures the input and output instances based on the user arguments and options.
848
     */
849
    protected function configureIO(InputInterface $input, OutputInterface $output)
850
    {
851
        if (true === $input->hasParameterOption(['--ansi'], true)) {
852
            $output->setDecorated(true);
853
        } elseif (true === $input->hasParameterOption(['--no-ansi'], true)) {
854
            $output->setDecorated(false);
855
        }
856
857
        if (true === $input->hasParameterOption(['--no-interaction', '-n'], true)) {
858
            $input->setInteractive(false);
859
        }
860
861
        switch ($shellVerbosity = (int) getenv('SHELL_VERBOSITY')) {
862
            case -1: $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); break;
863
            case 1: $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); break;
864
            case 2: $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); break;
865
            case 3: $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); break;
866
            default: $shellVerbosity = 0; break;
867
        }
868
869
        if (true === $input->hasParameterOption(['--quiet', '-q'], true)) {
870
            $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
871
            $shellVerbosity = -1;
872
        } else {
873
            if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) {
874
                $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
875
                $shellVerbosity = 3;
876
            } elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) {
877
                $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE);
878
                $shellVerbosity = 2;
879
            } elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) {
880
                $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
881
                $shellVerbosity = 1;
882
            }
883
        }
884
885
        if (-1 === $shellVerbosity) {
886
            $input->setInteractive(false);
887
        }
888
889
        putenv('SHELL_VERBOSITY='.$shellVerbosity);
890
        $_ENV['SHELL_VERBOSITY'] = $shellVerbosity;
891
        $_SERVER['SHELL_VERBOSITY'] = $shellVerbosity;
892
    }
893
894
    /**
895
     * Runs the current command.
896
     *
897
     * If an event dispatcher has been attached to the application,
898
     * events are also dispatched during the life-cycle of the command.
899
     *
900
     * @return int 0 if everything went fine, or an error code
901
     */
902
    protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
903
    {
904
        foreach ($command->getHelperSet() as $helper) {
905
            if ($helper instanceof InputAwareInterface) {
906
                $helper->setInput($input);
907
            }
908
        }
909
910
        if (null === $this->dispatcher) {
911
            return $command->run($input, $output);
912
        }
913
914
        // bind before the console.command event, so the listeners have access to input options/arguments
915
        try {
916
            $command->mergeApplicationDefinition();
917
            $input->bind($command->getDefinition());
918
        } catch (ExceptionInterface $e) {
919
            // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
920
        }
921
922
        $event = new ConsoleCommandEvent($command, $input, $output);
923
        $e = null;
924
925
        try {
926
            $this->dispatcher->dispatch($event, ConsoleEvents::COMMAND);
927
928
            if ($event->commandShouldRun()) {
929
                $exitCode = $command->run($input, $output);
930
            } else {
931
                $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED;
932
            }
933
        } catch (\Throwable $e) {
934
            $event = new ConsoleErrorEvent($input, $output, $e, $command);
935
            $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
936
            $e = $event->getError();
937
938
            if (0 === $exitCode = $event->getExitCode()) {
939
                $e = null;
940
            }
941
        }
942
943
        $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode);
944
        $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE);
945
946
        if (null !== $e) {
947
            throw $e;
948
        }
949
950
        return $event->getExitCode();
951
    }
952
953
    /**
954
     * Gets the name of the command based on input.
955
     *
956
     * @return string|null
957
     */
958
    protected function getCommandName(InputInterface $input)
959
    {
960
        return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument();
961
    }
962
963
    /**
964
     * Gets the default input definition.
965
     *
966
     * @return InputDefinition An InputDefinition instance
967
     */
968
    protected function getDefaultInputDefinition()
969
    {
970
        return new InputDefinition([
971
            new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
972
973
            new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
974
            new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
975
            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'),
976
            new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'),
977
            new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
978
            new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
979
            new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
980
        ]);
981
    }
982
983
    /**
984
     * Gets the default commands that should always be available.
985
     *
986
     * @return Command[] An array of default Command instances
987
     */
988
    protected function getDefaultCommands()
989
    {
990
        return [new HelpCommand(), new ListCommand()];
991
    }
992
993
    /**
994
     * Gets the default helper set with the helpers that should always be available.
995
     *
996
     * @return HelperSet A HelperSet instance
997
     */
998
    protected function getDefaultHelperSet()
999
    {
1000
        return new HelperSet([
1001
            new FormatterHelper(),
1002
            new DebugFormatterHelper(),
1003
            new ProcessHelper(),
1004
            new QuestionHelper(),
1005
        ]);
1006
    }
1007
1008
    /**
1009
     * Returns abbreviated suggestions in string format.
1010
     */
1011
    private function getAbbreviationSuggestions(array $abbrevs): string
1012
    {
1013
        return '    '.implode("\n    ", $abbrevs);
1014
    }
1015
1016
    /**
1017
     * Returns the namespace part of the command name.
1018
     *
1019
     * This method is not part of public API and should not be used directly.
1020
     *
1021
     * @return string The namespace of the command
1022
     */
1023
    public function extractNamespace(string $name, int $limit = null)
1024
    {
1025
        $parts = explode(':', $name, -1);
1026
1027
        return implode(':', null === $limit ? $parts : \array_slice($parts, 0, $limit));
1028
    }
1029
1030
    /**
1031
     * Finds alternative of $name among $collection,
1032
     * if nothing is found in $collection, try in $abbrevs.
1033
     *
1034
     * @return string[] A sorted array of similar string
1035
     */
1036
    private function findAlternatives(string $name, iterable $collection): array
1037
    {
1038
        $threshold = 1e3;
1039
        $alternatives = [];
1040
1041
        $collectionParts = [];
1042
        foreach ($collection as $item) {
1043
            $collectionParts[$item] = explode(':', $item);
1044
        }
1045
1046
        foreach (explode(':', $name) as $i => $subname) {
1047
            foreach ($collectionParts as $collectionName => $parts) {
1048
                $exists = isset($alternatives[$collectionName]);
1049
                if (!isset($parts[$i]) && $exists) {
1050
                    $alternatives[$collectionName] += $threshold;
1051
                    continue;
1052
                } elseif (!isset($parts[$i])) {
1053
                    continue;
1054
                }
1055
1056
                $lev = levenshtein($subname, $parts[$i]);
1057
                if ($lev <= \strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) {
1058
                    $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
1059
                } elseif ($exists) {
1060
                    $alternatives[$collectionName] += $threshold;
1061
                }
1062
            }
1063
        }
1064
1065
        foreach ($collection as $item) {
1066
            $lev = levenshtein($name, $item);
1067
            if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) {
1068
                $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
1069
            }
1070
        }
1071
1072
        $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
1073
        ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
1074
1075
        return array_keys($alternatives);
1076
    }
1077
1078
    /**
1079
     * Sets the default Command name.
1080
     *
1081
     * @return self
1082
     */
1083
    public function setDefaultCommand(string $commandName, bool $isSingleCommand = false)
1084
    {
1085
        $this->defaultCommand = $commandName;
1086
1087
        if ($isSingleCommand) {
1088
            // Ensure the command exist
1089
            $this->find($commandName);
1090
1091
            $this->singleCommand = true;
1092
        }
1093
1094
        return $this;
1095
    }
1096
1097
    /**
1098
     * @internal
1099
     */
1100
    public function isSingleCommand(): bool
1101
    {
1102
        return $this->singleCommand;
1103
    }
1104
1105
    private function splitStringByWidth(string $string, int $width): array
1106
    {
1107
        // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly.
1108
        // additionally, array_slice() is not enough as some character has doubled width.
1109
        // we need a function to split string not by character count but by string width
1110
        if (false === $encoding = mb_detect_encoding($string, null, true)) {
1111
            return str_split($string, $width);
1112
        }
1113
1114
        $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
1115
        $lines = [];
1116
        $line = '';
1117
1118
        $offset = 0;
1119
        while (preg_match('/.{1,10000}/u', $utf8String, $m, 0, $offset)) {
1120
            $offset += \strlen($m[0]);
1121
1122
            foreach (preg_split('//u', $m[0]) as $char) {
1123
                // test if $char could be appended to current line
1124
                if (mb_strwidth($line.$char, 'utf8') <= $width) {
1125
                    $line .= $char;
1126
                    continue;
1127
                }
1128
                // if not, push current line to array and make new line
1129
                $lines[] = str_pad($line, $width);
1130
                $line = $char;
1131
            }
1132
        }
1133
1134
        $lines[] = \count($lines) ? str_pad($line, $width) : $line;
1135
1136
        mb_convert_variables($encoding, 'utf8', $lines);
1137
1138
        return $lines;
1139
    }
1140
1141
    /**
1142
     * Returns all namespaces of the command name.
1143
     *
1144
     * @return string[] The namespaces of the command
1145
     */
1146
    private function extractAllNamespaces(string $name): array
1147
    {
1148
        // -1 as third argument is needed to skip the command short name when exploding
1149
        $parts = explode(':', $name, -1);
1150
        $namespaces = [];
1151
1152
        foreach ($parts as $part) {
1153
            if (\count($namespaces)) {
1154
                $namespaces[] = end($namespaces).':'.$part;
1155
            } else {
1156
                $namespaces[] = $part;
1157
            }
1158
        }
1159
1160
        return $namespaces;
1161
    }
1162
1163
    private function init()
1164
    {
1165
        if ($this->initialized) {
1166
            return;
1167
        }
1168
        $this->initialized = true;
1169
1170
        foreach ($this->getDefaultCommands() as $command) {
1171
            $this->add($command);
1172
        }
1173
    }
1174
}
1175