Console   F
last analyzed

Complexity

Total Complexity 118

Size/Duplication

Total Lines 737
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 7
Bugs 1 Features 0
Metric Value
eloc 273
c 7
b 1
f 0
dl 0
loc 737
ccs 0
cts 279
cp 0
rs 2
wmc 118

33 Methods

Rating   Name   Duplication   Size   Complexity  
A initialize() 0 6 2
A __construct() 0 12 1
C findAlternatives() 0 42 16
B makeRequest() 0 40 6
A getDefaultInputDefinition() 0 11 1
A extractNamespace() 0 6 2
A getHelp() 0 3 1
A call() 0 11 1
A getNamespaces() 0 16 4
A hasCommand() 0 3 1
A doRunCommand() 0 3 1
C configureIO() 0 20 15
A flushStartCallbacks() 0 3 1
A setUser() 0 8 3
A setDefinition() 0 3 1
A start() 0 4 2
B findNamespace() 0 30 7
A getLongVersion() 0 7 2
A doRun() 0 27 5
A getAbbreviationSuggestions() 0 3 2
A getDefinition() 0 3 1
A setAutoExit() 0 3 1
A all() 0 14 4
A getCommand() 0 26 4
A addCommands() 0 6 4
B run() 0 36 7
A getCommandName() 0 3 2
A setCatchExceptions() 0 3 1
B find() 0 37 9
A addCommand() 0 31 6
A loadCommands() 0 6 1
A extractAllNamespaces() 0 14 3
A starting() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Console 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 Console, and based on these observations, apply Extract Interface, too.

1
<?php
2
// +----------------------------------------------------------------------
3
// | TopThink [ WE CAN DO IT JUST THINK IT ]
4
// +----------------------------------------------------------------------
5
// | Copyright (c) 2015 http://www.topthink.com All rights reserved.
6
// +----------------------------------------------------------------------
7
// | Author: zhangyajun <[email protected]>
8
// +----------------------------------------------------------------------
9
declare (strict_types = 1);
10
11
namespace think;
12
13
use Closure;
14
use InvalidArgumentException;
15
use LogicException;
16
use think\console\Command;
17
use think\console\command\Clear;
18
use think\console\command\Help;
19
use think\console\command\Help as HelpCommand;
20
use think\console\command\Lists;
21
use think\console\command\make\Command as MakeCommand;
22
use think\console\command\make\Controller;
23
use think\console\command\make\Event;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, think\Event. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
24
use think\console\command\make\Listener;
25
use think\console\command\make\Middleware;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, think\Middleware. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
26
use think\console\command\make\Model;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, think\Model. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
27
use think\console\command\make\Service;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, think\Service. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
28
use think\console\command\make\Subscribe;
29
use think\console\command\make\Validate;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, think\Validate. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
30
use think\console\command\optimize\Route;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, think\Route. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
31
use think\console\command\optimize\Schema;
32
use think\console\command\RouteList;
33
use think\console\command\RunServer;
34
use think\console\command\ServiceDiscover;
35
use think\console\command\VendorPublish;
36
use think\console\command\Version;
37
use think\console\Input;
38
use think\console\input\Argument as InputArgument;
39
use think\console\input\Definition as InputDefinition;
40
use think\console\input\Option as InputOption;
41
use think\console\Output;
42
use think\console\output\driver\Buffer;
43
44
/**
45
 * 控制台应用管理类
46
 */
47
class Console
48
{
49
50
    protected $app;
51
52
    /** @var Command[] */
53
    protected $commands = [];
54
55
    protected $wantHelps = false;
56
57
    protected $catchExceptions = true;
58
    protected $autoExit        = true;
59
    protected $definition;
60
    protected $defaultCommand  = 'list';
61
62
    protected $defaultCommands = [
63
        'help'             => Help::class,
64
        'list'             => Lists::class,
65
        'clear'            => Clear::class,
66
        'make:command'     => MakeCommand::class,
67
        'make:controller'  => Controller::class,
68
        'make:model'       => Model::class,
69
        'make:middleware'  => Middleware::class,
70
        'make:validate'    => Validate::class,
71
        'make:event'       => Event::class,
72
        'make:listener'    => Listener::class,
73
        'make:service'     => Service::class,
74
        'make:subscribe'   => Subscribe::class,
75
        'optimize:route'   => Route::class,
76
        'optimize:schema'  => Schema::class,
77
        'run'              => RunServer::class,
78
        'version'          => Version::class,
79
        'route:list'       => RouteList::class,
80
        'service:discover' => ServiceDiscover::class,
81
        'vendor:publish'   => VendorPublish::class,
82
    ];
83
84
    /**
85
     * 启动器
86
     * @var array
87
     */
88
    protected static $startCallbacks = [];
89
90
    public function __construct(App $app)
91
    {
92
        $this->app = $app;
93
94
        $this->initialize();
95
96
        $this->definition = $this->getDefaultInputDefinition();
97
98
        //加载指令
99
        $this->loadCommands();
100
101
        $this->start();
102
    }
103
104
    /**
105
     * 初始化
106
     */
107
    protected function initialize()
108
    {
109
        if (!$this->app->initialized()) {
110
            $this->app->initialize();
111
        }
112
        $this->makeRequest();
113
    }
114
115
    /**
116
     * 构造request
117
     */
118
    protected function makeRequest()
119
    {
120
        $url = $this->app->config->get('app.url', 'http://localhost');
121
122
        $components = parse_url($url);
0 ignored issues
show
Bug introduced by
It seems like $url can also be of type array; however, parameter $url of parse_url() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

122
        $components = parse_url(/** @scrutinizer ignore-type */ $url);
Loading history...
123
124
        $server = $_SERVER;
125
126
        if (isset($components['path'])) {
127
            $server = array_merge($server, [
128
                'SCRIPT_FILENAME' => $components['path'],
129
                'SCRIPT_NAME'     => $components['path'],
130
                'REQUEST_URI'     => $components['path'],
131
            ]);
132
        }
133
134
        if (isset($components['host'])) {
135
            $server['SERVER_NAME'] = $components['host'];
136
            $server['HTTP_HOST']   = $components['host'];
137
        }
138
139
        if (isset($components['scheme'])) {
140
            if ('https' === $components['scheme']) {
141
                $server['HTTPS']       = 'on';
142
                $server['SERVER_PORT'] = 443;
143
            } else {
144
                unset($server['HTTPS']);
145
                $server['SERVER_PORT'] = 80;
146
            }
147
        }
148
149
        if (isset($components['port'])) {
150
            $server['SERVER_PORT'] = $components['port'];
151
            $server['HTTP_HOST'] .= ':' . $components['port'];
152
        }
153
154
        /** @var Request $request */
155
        $request = $this->app->make('request');
156
157
        $request->withServer($server);
158
    }
159
160
    /**
161
     * 添加初始化器
162
     * @param Closure $callback
163
     */
164
    public static function starting(Closure $callback): void
165
    {
166
        static::$startCallbacks[] = $callback;
167
    }
168
169
    /**
170
     * 清空启动器
171
     */
172
    public static function flushStartCallbacks(): void
173
    {
174
        static::$startCallbacks = [];
175
    }
176
177
    /**
178
     * 设置执行用户
179
     * @param $user
180
     */
181
    public static function setUser(string $user): void
182
    {
183
        if (extension_loaded('posix')) {
184
            $user = posix_getpwnam($user);
185
186
            if (!empty($user)) {
187
                posix_setgid($user['gid']);
188
                posix_setuid($user['uid']);
189
            }
190
        }
191
    }
192
193
    /**
194
     * 启动
195
     */
196
    protected function start(): void
197
    {
198
        foreach (static::$startCallbacks as $callback) {
199
            $callback($this);
200
        }
201
    }
202
203
    /**
204
     * 加载指令
205
     * @access protected
206
     */
207
    protected function loadCommands(): void
208
    {
209
        $commands = $this->app->config->get('console.commands', []);
210
        $commands = array_merge($this->defaultCommands, $commands);
211
212
        $this->addCommands($commands);
213
    }
214
215
    /**
216
     * @access public
217
     * @param string $command
218
     * @param array $parameters
219
     * @param string $driver
220
     * @return Output|Buffer
221
     */
222
    public function call(string $command, array $parameters = [], string $driver = 'buffer')
223
    {
224
        array_unshift($parameters, $command);
225
226
        $input  = new Input($parameters);
227
        $output = new Output($driver);
228
229
        $this->setCatchExceptions(false);
230
        $this->find($command)->run($input, $output);
231
232
        return $output;
233
    }
234
235
    /**
236
     * 执行当前的指令
237
     * @access public
238
     * @return int
239
     * @throws \Exception
240
     * @api
241
     */
242
    public function run()
243
    {
244
        $input  = new Input();
245
        $output = new Output();
246
247
        $this->configureIO($input, $output);
248
249
        try {
250
            $exitCode = $this->doRun($input, $output);
251
        } catch (\Exception $e) {
252
            if (!$this->catchExceptions) {
253
                throw $e;
254
            }
255
256
            $output->renderException($e);
257
258
            $exitCode = $e->getCode();
259
            if (is_numeric($exitCode)) {
260
                $exitCode = (int) $exitCode;
261
                if (0 === $exitCode) {
262
                    $exitCode = 1;
263
                }
264
            } else {
265
                $exitCode = 1;
266
            }
267
        }
268
269
        if ($this->autoExit) {
270
            if ($exitCode > 255) {
271
                $exitCode = 255;
272
            }
273
274
            exit($exitCode);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
275
        }
276
277
        return $exitCode;
278
    }
279
280
    /**
281
     * 执行指令
282
     * @access public
283
     * @param Input $input
284
     * @param Output $output
285
     * @return int
286
     */
287
    public function doRun(Input $input, Output $output)
288
    {
289
        if (true === $input->hasParameterOption(['--version', '-V'])) {
290
            $output->writeln($this->getLongVersion());
291
292
            return 0;
293
        }
294
295
        $name = $this->getCommandName($input);
296
297
        if (true === $input->hasParameterOption(['--help', '-h'])) {
298
            if (!$name) {
299
                $name  = 'help';
300
                $input = new Input(['help']);
301
            } else {
302
                $this->wantHelps = true;
303
            }
304
        }
305
306
        if (!$name) {
307
            $name  = $this->defaultCommand;
308
            $input = new Input([$this->defaultCommand]);
309
        }
310
311
        $command = $this->find($name);
312
313
        return $this->doRunCommand($command, $input, $output);
314
    }
315
316
    /**
317
     * 设置输入参数定义
318
     * @access public
319
     * @param InputDefinition $definition
320
     */
321
    public function setDefinition(InputDefinition $definition): void
322
    {
323
        $this->definition = $definition;
324
    }
325
326
    /**
327
     * 获取输入参数定义
328
     * @access public
329
     * @return InputDefinition The InputDefinition instance
330
     */
331
    public function getDefinition(): InputDefinition
332
    {
333
        return $this->definition;
334
    }
335
336
    /**
337
     * Gets the help message.
338
     * @access public
339
     * @return string A help message.
340
     */
341
    public function getHelp(): string
342
    {
343
        return $this->getLongVersion();
344
    }
345
346
    /**
347
     * 是否捕获异常
348
     * @access public
349
     * @param bool $boolean
350
     * @api
351
     */
352
    public function setCatchExceptions(bool $boolean): void
353
    {
354
        $this->catchExceptions = $boolean;
355
    }
356
357
    /**
358
     * 是否自动退出
359
     * @access public
360
     * @param bool $boolean
361
     * @api
362
     */
363
    public function setAutoExit(bool $boolean): void
364
    {
365
        $this->autoExit = $boolean;
366
    }
367
368
    /**
369
     * 获取完整的版本号
370
     * @access public
371
     * @return string
372
     */
373
    public function getLongVersion(): string
374
    {
375
        if ($this->app->version()) {
376
            return sprintf('version <comment>%s</comment>', $this->app->version());
377
        }
378
379
        return '<info>Console Tool</info>';
380
    }
381
382
    /**
383
     * 添加指令集
384
     * @access public
385
     * @param array $commands
386
     */
387
    public function addCommands(array $commands): void
388
    {
389
        foreach ($commands as $key => $command) {
390
            if (is_subclass_of($command, Command::class)) {
391
                // 注册指令
392
                $this->addCommand($command, is_numeric($key) ? '' : $key);
393
            }
394
        }
395
    }
396
397
    /**
398
     * 添加一个指令
399
     * @access public
400
     * @param string|Command $command 指令对象或者指令类名
401
     * @param string $name 指令名 留空则自动获取
402
     * @return Command|void
403
     */
404
    public function addCommand($command, string $name = '')
405
    {
406
        if ($name) {
407
            $this->commands[$name] = $command;
408
            return;
409
        }
410
411
        if (is_string($command)) {
412
            $command = $this->app->invokeClass($command);
413
        }
414
415
        $command->setConsole($this);
416
417
        if (!$command->isEnabled()) {
418
            $command->setConsole(null);
419
            return;
420
        }
421
422
        $command->setApp($this->app);
423
424
        if (null === $command->getDefinition()) {
425
            throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command)));
426
        }
427
428
        $this->commands[$command->getName()] = $command;
429
430
        foreach ($command->getAliases() as $alias) {
431
            $this->commands[$alias] = $command;
432
        }
433
434
        return $command;
435
    }
436
437
    /**
438
     * 获取指令
439
     * @access public
440
     * @param string $name 指令名称
441
     * @return Command
442
     * @throws InvalidArgumentException
443
     */
444
    public function getCommand(string $name): Command
445
    {
446
        if (!isset($this->commands[$name])) {
447
            throw new InvalidArgumentException(sprintf('The command "%s" does not exist.', $name));
448
        }
449
450
        $command = $this->commands[$name];
451
452
        if (is_string($command)) {
0 ignored issues
show
introduced by
The condition is_string($command) is always false.
Loading history...
453
            $command = $this->app->invokeClass($command);
454
            /** @var Command $command */
455
            $command->setConsole($this);
456
            $command->setApp($this->app);
457
        }
458
459
        if ($this->wantHelps) {
460
            $this->wantHelps = false;
461
462
            /** @var HelpCommand $helpCommand */
463
            $helpCommand = $this->getCommand('help');
464
            $helpCommand->setCommand($command);
465
466
            return $helpCommand;
467
        }
468
469
        return $command;
470
    }
471
472
    /**
473
     * 某个指令是否存在
474
     * @access public
475
     * @param string $name 指令名称
476
     * @return bool
477
     */
478
    public function hasCommand(string $name): bool
479
    {
480
        return isset($this->commands[$name]);
481
    }
482
483
    /**
484
     * 获取所有的命名空间
485
     * @access public
486
     * @return array
487
     */
488
    public function getNamespaces(): array
489
    {
490
        $namespaces = [];
491
        foreach ($this->commands as $key => $command) {
492
            if (is_string($command)) {
493
                $namespaces = array_merge($namespaces, $this->extractAllNamespaces($key));
494
            } else {
495
                $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName()));
496
497
                foreach ($command->getAliases() as $alias) {
498
                    $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias));
499
                }
500
            }
501
        }
502
503
        return array_values(array_unique(array_filter($namespaces)));
504
    }
505
506
    /**
507
     * 查找注册命名空间中的名称或缩写。
508
     * @access public
509
     * @param string $namespace
510
     * @return string
511
     * @throws InvalidArgumentException
512
     */
513
    public function findNamespace(string $namespace): string
514
    {
515
        $allNamespaces = $this->getNamespaces();
516
        $expr          = preg_replace_callback('{([^:]+|)}', function ($matches) {
517
            return preg_quote($matches[1]) . '[^:]*';
518
        }, $namespace);
519
        $namespaces    = preg_grep('{^' . $expr . '}', $allNamespaces);
520
521
        if (empty($namespaces)) {
522
            $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
523
524
            if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
525
                if (1 == count($alternatives)) {
526
                    $message .= "\n\nDid you mean this?\n    ";
527
                } else {
528
                    $message .= "\n\nDid you mean one of these?\n    ";
529
                }
530
531
                $message .= implode("\n    ", $alternatives);
532
            }
533
534
            throw new InvalidArgumentException($message);
535
        }
536
537
        $exact = in_array($namespace, $namespaces, true);
538
        if (count($namespaces) > 1 && !$exact) {
539
            throw new InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))));
540
        }
541
542
        return $exact ? $namespace : reset($namespaces);
543
    }
544
545
    /**
546
     * 查找指令
547
     * @access public
548
     * @param string $name 名称或者别名
549
     * @return Command
550
     * @throws InvalidArgumentException
551
     */
552
    public function find(string $name): Command
553
    {
554
        $allCommands = array_keys($this->commands);
555
556
        $expr = preg_replace_callback('{([^:]+|)}', function ($matches) {
557
            return preg_quote($matches[1]) . '[^:]*';
558
        }, $name);
559
560
        $commands = preg_grep('{^' . $expr . '}', $allCommands);
561
562
        if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) {
563
            if (false !== $pos = strrpos($name, ':')) {
564
                $this->findNamespace(substr($name, 0, $pos));
565
            }
566
567
            $message = sprintf('Command "%s" is not defined.', $name);
568
569
            if ($alternatives = $this->findAlternatives($name, $allCommands)) {
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
                $message .= implode("\n    ", $alternatives);
576
            }
577
578
            throw new InvalidArgumentException($message);
579
        }
580
581
        $exact = in_array($name, $commands, true);
582
        if (count($commands) > 1 && !$exact) {
583
            $suggestions = $this->getAbbreviationSuggestions(array_values($commands));
584
585
            throw new InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions));
586
        }
587
588
        return $this->getCommand($exact ? $name : reset($commands));
589
    }
590
591
    /**
592
     * 获取所有的指令
593
     * @access public
594
     * @param string $namespace 命名空间
595
     * @return Command[]
596
     * @api
597
     */
598
    public function all(string $namespace = null): array
599
    {
600
        if (null === $namespace) {
601
            return $this->commands;
602
        }
603
604
        $commands = [];
605
        foreach ($this->commands as $name => $command) {
606
            if ($this->extractNamespace($name, substr_count($namespace, ':') + 1) === $namespace) {
607
                $commands[$name] = $command;
608
            }
609
        }
610
611
        return $commands;
612
    }
613
614
    /**
615
     * 配置基于用户的参数和选项的输入和输出实例。
616
     * @access protected
617
     * @param Input $input 输入实例
618
     * @param Output $output 输出实例
619
     */
620
    protected function configureIO(Input $input, Output $output): void
621
    {
622
        if (true === $input->hasParameterOption(['--ansi'])) {
623
            $output->setDecorated(true);
624
        } elseif (true === $input->hasParameterOption(['--no-ansi'])) {
625
            $output->setDecorated(false);
626
        }
627
628
        if (true === $input->hasParameterOption(['--no-interaction', '-n'])) {
629
            $input->setInteractive(false);
630
        }
631
632
        if (true === $input->hasParameterOption(['--quiet', '-q'])) {
633
            $output->setVerbosity(Output::VERBOSITY_QUIET);
634
        } elseif ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) {
635
            $output->setVerbosity(Output::VERBOSITY_DEBUG);
636
        } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) {
637
            $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE);
638
        } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) {
639
            $output->setVerbosity(Output::VERBOSITY_VERBOSE);
640
        }
641
    }
642
643
    /**
644
     * 执行指令
645
     * @access protected
646
     * @param Command $command 指令实例
647
     * @param Input $input 输入实例
648
     * @param Output $output 输出实例
649
     * @return int
650
     * @throws \Exception
651
     */
652
    protected function doRunCommand(Command $command, Input $input, Output $output)
653
    {
654
        return $command->run($input, $output);
655
    }
656
657
    /**
658
     * 获取指令的基础名称
659
     * @access protected
660
     * @param Input $input
661
     * @return string
662
     */
663
    protected function getCommandName(Input $input): string
664
    {
665
        return $input->getFirstArgument() ?: '';
666
    }
667
668
    /**
669
     * 获取默认输入定义
670
     * @access protected
671
     * @return InputDefinition
672
     */
673
    protected function getDefaultInputDefinition(): InputDefinition
674
    {
675
        return new InputDefinition([
676
            new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
677
            new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
678
            new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this console version'),
679
            new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
680
            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'),
681
            new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
682
            new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
683
            new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
684
        ]);
685
    }
686
687
    /**
688
     * 获取可能的建议
689
     * @access private
690
     * @param array $abbrevs
691
     * @return string
692
     */
693
    private function getAbbreviationSuggestions(array $abbrevs): string
694
    {
695
        return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : '');
696
    }
697
698
    /**
699
     * 返回命名空间部分
700
     * @access public
701
     * @param string $name 指令
702
     * @param int $limit 部分的命名空间的最大数量
703
     * @return string
704
     */
705
    public function extractNamespace(string $name, int $limit = 0): string
706
    {
707
        $parts = explode(':', $name);
708
        array_pop($parts);
709
710
        return implode(':', 0 === $limit ? $parts : array_slice($parts, 0, $limit));
711
    }
712
713
    /**
714
     * 查找可替代的建议
715
     * @access private
716
     * @param string $name
717
     * @param array|\Traversable $collection
718
     * @return array
719
     */
720
    private function findAlternatives(string $name, $collection): array
721
    {
722
        $threshold    = 1e3;
723
        $alternatives = [];
724
725
        $collectionParts = [];
726
        foreach ($collection as $item) {
727
            $collectionParts[$item] = explode(':', $item);
728
        }
729
730
        foreach (explode(':', $name) as $i => $subname) {
731
            foreach ($collectionParts as $collectionName => $parts) {
732
                $exists = isset($alternatives[$collectionName]);
733
                if (!isset($parts[$i]) && $exists) {
734
                    $alternatives[$collectionName] += $threshold;
735
                    continue;
736
                } elseif (!isset($parts[$i])) {
737
                    continue;
738
                }
739
740
                $lev = levenshtein($subname, $parts[$i]);
741
                if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) {
742
                    $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
743
                } elseif ($exists) {
744
                    $alternatives[$collectionName] += $threshold;
745
                }
746
            }
747
        }
748
749
        foreach ($collection as $item) {
750
            $lev = levenshtein($name, $item);
751
            if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
752
                $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
753
            }
754
        }
755
756
        $alternatives = array_filter($alternatives, function ($lev) use ($threshold) {
757
            return $lev < 2 * $threshold;
758
        });
759
        asort($alternatives);
760
761
        return array_keys($alternatives);
762
    }
763
764
    /**
765
     * 返回所有的命名空间
766
     * @access private
767
     * @param string $name
768
     * @return array
769
     */
770
    private function extractAllNamespaces(string $name): array
771
    {
772
        $parts      = explode(':', $name, -1);
773
        $namespaces = [];
774
775
        foreach ($parts as $part) {
776
            if (count($namespaces)) {
777
                $namespaces[] = end($namespaces) . ':' . $part;
778
            } else {
779
                $namespaces[] = $part;
780
            }
781
        }
782
783
        return $namespaces;
784
    }
785
786
}
787