Passed
Push — 5.2 ( 3b89d2...9e499a )
by liu
02:30
created

Console::find()   B

Complexity

Conditions 9
Paths 8

Size

Total Lines 37
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 21
nc 8
nop 1
dl 0
loc 37
rs 8.0555
c 0
b 0
f 0
1
<?php
2
// +----------------------------------------------------------------------
1 ignored issue
show
Coding Style introduced by
You must use "/**" style comments for a file comment
Loading history...
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 think\console\Command;
15
use think\console\command\Clear;
16
use think\console\command\Help;
17
use think\console\command\Help as HelpCommand;
18
use think\console\command\Lists;
19
use think\console\command\make\Command as MakeCommand;
20
use think\console\command\make\Controller;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, think\Controller. 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...
21
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...
22
use think\console\command\make\Listener;
23
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...
24
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...
25
use think\console\command\make\Subscribe;
26
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...
27
use think\console\command\optimize\Config;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, think\Config. 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\optimize\Facade;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, think\Facade. 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...
29
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...
30
use think\console\command\optimize\Schema;
31
use think\console\command\RouteList;
32
use think\console\command\RunServer;
33
use think\console\command\ServiceDiscover;
34
use think\console\command\Version;
35
use think\console\Input;
36
use think\console\input\Argument as InputArgument;
37
use think\console\input\Definition as InputDefinition;
38
use think\console\input\Option as InputOption;
39
use think\console\Output;
40
use think\console\output\driver\Buffer;
41
42
/**
43
 * 控制台应用管理类
44
 */
5 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @package tag in class comment
Loading history...
Coding Style introduced by
Missing @author tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
45
class Console
46
{
47
48
    protected $app;
49
50
    /** @var Command[] */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
51
    protected $commands = [];
52
53
    protected $wantHelps = false;
54
55
    protected $catchExceptions = true;
56
    protected $autoExit        = true;
57
    protected $definition;
58
    protected $defaultCommand = 'list';
59
60
    protected $defaultCommands = [
61
        'help'             => Help::class,
62
        'list'             => Lists::class,
63
        'build'            => Build::class,
64
        'clear'            => Clear::class,
65
        'make:command'     => MakeCommand::class,
66
        'make:controller'  => Controller::class,
67
        'make:model'       => Model::class,
68
        'make:middleware'  => Middleware::class,
69
        'make:validate'    => Validate::class,
70
        'make:event'       => Event::class,
71
        'make:listener'    => Listener::class,
72
        'make:subscribe'   => Subscribe::class,
73
        'optimize:config'  => Config::class,
74
        'optimize:schema'  => Schema::class,
75
        'optimize:route'   => Route::class,
76
        'optimize:facade'  => Facade::class,
77
        'run'              => RunServer::class,
78
        'version'          => Version::class,
79
        'route:list'       => RouteList::class,
80
        'service:discover' => ServiceDiscover::class,
81
    ];
82
83
    /**
84
     * 初始化器
85
     * @var array
86
     */
87
    protected static $initializers = [];
88
89
    public function __construct(App $app)
0 ignored issues
show
Coding Style introduced by
Missing function doc comment
Loading history...
90
    {
91
        $this->app = $app;
92
93
        $this->app->initialize();
94
95
        $user = $this->app->config->get('console.user');
96
97
        if ($user) {
98
            $this->setUser($user);
0 ignored issues
show
Bug introduced by
It seems like $user can also be of type array; however, parameter $user of think\Console::setUser() 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

98
            $this->setUser(/** @scrutinizer ignore-type */ $user);
Loading history...
99
        }
100
101
        $this->definition = $this->getDefaultInputDefinition();
102
103
        //加载指令
104
        $this->loadCommands();
105
106
        $this->initialize();
107
    }
108
109
    /**
110
     * 添加初始化器
111
     * @param Closure $callback
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
112
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
113
    public static function init(Closure $callback): void
114
    {
115
        static::$initializers[] = $callback;
116
    }
117
118
    /**
119
     * 清空初始化器
120
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
121
    public static function flushInit(): void
122
    {
123
        static::$initializers = [];
124
    }
125
126
    /**
127
     * 初始化
128
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
129
    protected function initialize(): void
130
    {
131
        foreach (static::$initializers as $initialize) {
132
            $initialize($this);
133
        }
134
    }
135
136
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $user should have a doc-comment as per coding-style.
Loading history...
137
     * 设置执行用户
138
     * @param $user
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
139
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
140
    protected function setUser(string $user): void
141
    {
142
        if (function_exists('posix_getpwnam')) {
143
            $user = posix_getpwnam($user);
144
145
            if (!empty($user)) {
146
                posix_setuid($user['uid']);
147
                posix_setgid($user['gid']);
148
            }
149
        }
150
    }
151
152
    /**
153
     * 加载指令
154
     * @access protected
155
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
156
    protected function loadCommands(): void
157
    {
158
        $commands = $this->app->config->get('console.commands', []);
159
        $commands = array_merge($this->defaultCommands, $commands);
160
161
        $this->addCommands($commands);
162
    }
163
164
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
165
     * @access public
166
     * @param  string $command
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
167
     * @param  array  $parameters
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
168
     * @param  string $driver
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
169
     * @return Output|Buffer
170
     */
171
    public function call(string $command, array $parameters = [], string $driver = 'buffer')
172
    {
173
        array_unshift($parameters, $command);
174
175
        $input  = new Input($parameters);
176
        $output = new Output($driver);
177
178
        $this->setCatchExceptions(false);
179
        $this->find($command)->run($input, $output);
180
181
        return $output;
182
    }
183
184
    /**
185
     * 执行当前的指令
186
     * @access public
187
     * @return int
188
     * @throws \Exception
189
     * @api
190
     */
191
    public function run()
192
    {
193
        $input  = new Input();
194
        $output = new Output();
195
196
        $this->configureIO($input, $output);
197
198
        try {
199
            $exitCode = $this->doRun($input, $output);
200
        } catch (\Exception $e) {
201
            if (!$this->catchExceptions) {
202
                throw $e;
203
            }
204
205
            $output->renderException($e);
206
207
            $exitCode = $e->getCode();
208
            if (is_numeric($exitCode)) {
209
                $exitCode = (int) $exitCode;
210
                if (0 === $exitCode) {
211
                    $exitCode = 1;
212
                }
213
            } else {
214
                $exitCode = 1;
215
            }
216
        }
217
218
        if ($this->autoExit) {
219
            if ($exitCode > 255) {
220
                $exitCode = 255;
221
            }
222
223
            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...
224
        }
225
226
        return $exitCode;
227
    }
228
229
    /**
230
     * 执行指令
231
     * @access public
232
     * @param  Input  $input
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
233
     * @param  Output $output
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
234
     * @return int
235
     */
236
    public function doRun(Input $input, Output $output)
237
    {
238
        if (true === $input->hasParameterOption(['--version', '-V'])) {
239
            $output->writeln($this->getLongVersion());
240
241
            return 0;
242
        }
243
244
        $name = $this->getCommandName($input);
245
246
        if (true === $input->hasParameterOption(['--help', '-h'])) {
247
            if (!$name) {
248
                $name  = 'help';
249
                $input = new Input(['help']);
250
            } else {
251
                $this->wantHelps = true;
252
            }
253
        }
254
255
        if (!$name) {
256
            $name  = $this->defaultCommand;
257
            $input = new Input([$this->defaultCommand]);
258
        }
259
260
        $command = $this->find($name);
261
262
        return $this->doRunCommand($command, $input, $output);
263
    }
264
265
    /**
266
     * 设置输入参数定义
267
     * @access public
268
     * @param  InputDefinition $definition
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
269
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
270
    public function setDefinition(InputDefinition $definition): void
271
    {
272
        $this->definition = $definition;
273
    }
274
275
    /**
276
     * 获取输入参数定义
277
     * @access public
278
     * @return InputDefinition The InputDefinition instance
279
     */
280
    public function getDefinition(): InputDefinition
281
    {
282
        return $this->definition;
283
    }
284
285
    /**
286
     * Gets the help message.
287
     * @access public
288
     * @return string A help message.
289
     */
290
    public function getHelp(): string
291
    {
292
        return $this->getLongVersion();
293
    }
294
295
    /**
296
     * 是否捕获异常
297
     * @access public
298
     * @param  bool $boolean
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
299
     * @api
300
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
301
    public function setCatchExceptions(bool $boolean): void
302
    {
303
        $this->catchExceptions = $boolean;
304
    }
305
306
    /**
307
     * 是否自动退出
308
     * @access public
309
     * @param  bool $boolean
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
310
     * @api
311
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
312
    public function setAutoExit(bool $boolean): void
313
    {
314
        $this->autoExit = $boolean;
315
    }
316
317
    /**
318
     * 获取完整的版本号
319
     * @access public
320
     * @return string
321
     */
322
    public function getLongVersion(): string
323
    {
324
        if ('UNKNOWN' !== $this->app->getName() && 'UNKNOWN' !== $this->app->version()) {
325
            return sprintf('<info>%s</info> version <comment>%s</comment>', $this->app->getName(), $this->app->version());
326
        }
327
328
        return '<info>Console Tool</info>';
329
    }
330
331
    /**
332
     * 注册一个指令 (便于动态创建指令)
333
     * @access public
334
     * @param  string $name 指令名
335
     * @return Command
336
     */
337
    public function register(string $name)
338
    {
339
        $this->addCommand(new Command($name));
340
    }
341
342
    /**
343
     * 添加指令集
344
     * @access public
345
     * @param  array $commands
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
346
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
347
    public function addCommands(array $commands): void
348
    {
349
        foreach ($commands as $key => $command) {
350
            if (is_subclass_of($command, "\\think\\console\\Command")) {
351
                // 注册指令
352
                $this->addCommand($command, is_numeric($key) ? '' : $key);
353
            }
354
        }
355
    }
356
357
    /**
358
     * 添加一个指令
359
     * @access public
360
     * @param  string|Command $command 指令对象或者指令类名
361
     * @param  string         $name    指令名 留空则自动获取
362
     * @return Command|null
363
     */
364
    public function addCommand($command, string $name = '')
365
    {
366
        if ($name) {
367
            $this->commands[$name] = $command;
368
            return;
369
        }
370
371
        if (is_string($command)) {
372
            $command = new $command();
373
        }
374
375
        $command->setConsole($this);
376
377
        if (!$command->isEnabled()) {
378
            $command->setConsole(null);
379
            return;
380
        }
381
382
        $command->setApp($this->app);
383
384
        if (null === $command->getDefinition()) {
385
            throw new \LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command)));
386
        }
387
388
        $this->commands[$command->getName()] = $command;
389
390
        foreach ($command->getAliases() as $alias) {
391
            $this->commands[$alias] = $command;
392
        }
393
394
        return $command;
395
    }
396
397
    /**
398
     * 获取指令
399
     * @access public
400
     * @param  string $name 指令名称
401
     * @return Command
402
     * @throws \InvalidArgumentException
403
     */
404
    public function getCommand(string $name): Command
405
    {
406
        if (!isset($this->commands[$name])) {
407
            throw new \InvalidArgumentException(sprintf('The command "%s" does not exist.', $name));
408
        }
409
410
        $command = $this->commands[$name];
411
412
        if (is_string($command)) {
0 ignored issues
show
introduced by
The condition is_string($command) is always false.
Loading history...
413
            $command = new $command();
414
        }
415
416
        $command->setConsole($this);
417
418
        if ($this->wantHelps) {
419
            $this->wantHelps = false;
420
421
            /** @var HelpCommand $helpCommand */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
422
            $helpCommand = $this->getCommand('help');
423
            $helpCommand->setCommand($command);
424
425
            return $helpCommand;
426
        }
427
428
        return $command;
429
    }
430
431
    /**
432
     * 某个指令是否存在
433
     * @access public
434
     * @param  string $name 指令名称
435
     * @return bool
436
     */
437
    public function hasCommand(string $name): bool
438
    {
439
        return isset($this->commands[$name]);
440
    }
441
442
    /**
443
     * 获取所有的命名空间
444
     * @access public
445
     * @return array
446
     */
447
    public function getNamespaces(): array
448
    {
449
        $namespaces = [];
450
        foreach ($this->commands as $command) {
451
            if (is_string($command)) {
452
                $namespaces[] = $command;
453
            } else {
454
                $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName()));
455
456
                foreach ($command->getAliases() as $alias) {
457
                    $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias));
458
                }
459
            }
460
        }
461
462
        return array_values(array_unique(array_filter($namespaces)));
463
    }
464
465
    /**
466
     * 查找注册命名空间中的名称或缩写。
467
     * @access public
468
     * @param  string $namespace
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
469
     * @return string
470
     * @throws \InvalidArgumentException
471
     */
472
    public function findNamespace(string $namespace): string
473
    {
474
        $allNamespaces = $this->getNamespaces();
475
        $expr          = preg_replace_callback('{([^:]+|)}', function ($matches) {
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
476
            return preg_quote($matches[1]) . '[^:]*';
477
        }, $namespace);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
478
        $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces);
479
480
        if (empty($namespaces)) {
481
            $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
482
483
            if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
484
                if (1 == count($alternatives)) {
485
                    $message .= "\n\nDid you mean this?\n    ";
486
                } else {
487
                    $message .= "\n\nDid you mean one of these?\n    ";
488
                }
489
490
                $message .= implode("\n    ", $alternatives);
491
            }
492
493
            throw new \InvalidArgumentException($message);
494
        }
495
496
        $exact = in_array($namespace, $namespaces, true);
497
        if (count($namespaces) > 1 && !$exact) {
498
            throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))));
499
        }
500
501
        return $exact ? $namespace : reset($namespaces);
502
    }
503
504
    /**
505
     * 查找指令
506
     * @access public
507
     * @param  string $name 名称或者别名
508
     * @return Command
509
     * @throws \InvalidArgumentException
510
     */
511
    public function find(string $name): Command
512
    {
513
        $allCommands = array_keys($this->commands);
514
515
        $expr = preg_replace_callback('{([^:]+|)}', function ($matches) {
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
516
            return preg_quote($matches[1]) . '[^:]*';
517
        }, $name);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
518
519
        $commands = preg_grep('{^' . $expr . '}', $allCommands);
520
521
        if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) {
522
            if (false !== $pos = strrpos($name, ':')) {
523
                $this->findNamespace(substr($name, 0, $pos));
524
            }
525
526
            $message = sprintf('Command "%s" is not defined.', $name);
527
528
            if ($alternatives = $this->findAlternatives($name, $allCommands)) {
529
                if (1 == count($alternatives)) {
530
                    $message .= "\n\nDid you mean this?\n    ";
531
                } else {
532
                    $message .= "\n\nDid you mean one of these?\n    ";
533
                }
534
                $message .= implode("\n    ", $alternatives);
535
            }
536
537
            throw new \InvalidArgumentException($message);
538
        }
539
540
        $exact = in_array($name, $commands, true);
541
        if (count($commands) > 1 && !$exact) {
542
            $suggestions = $this->getAbbreviationSuggestions(array_values($commands));
543
544
            throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions));
545
        }
546
547
        return $this->getCommand($exact ? $name : reset($commands));
548
    }
549
550
    /**
551
     * 获取所有的指令
552
     * @access public
553
     * @param  string $namespace 命名空间
554
     * @return Command[]
555
     * @api
556
     */
557
    public function all(string $namespace = null): array
558
    {
559
        if (null === $namespace) {
560
            return $this->commands;
561
        }
562
563
        $commands = [];
564
        foreach ($this->commands as $name => $command) {
565
            if ($this->extractNamespace($name, substr_count($namespace, ':') + 1) === $namespace) {
566
                $commands[$name] = $command;
567
            }
568
        }
569
570
        return $commands;
571
    }
572
573
    /**
574
     * 配置基于用户的参数和选项的输入和输出实例。
575
     * @access protected
576
     * @param  Input  $input  输入实例
577
     * @param  Output $output 输出实例
578
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
579
    protected function configureIO(Input $input, Output $output): void
580
    {
581
        if (true === $input->hasParameterOption(['--ansi'])) {
582
            $output->setDecorated(true);
583
        } elseif (true === $input->hasParameterOption(['--no-ansi'])) {
584
            $output->setDecorated(false);
585
        }
586
587
        if (true === $input->hasParameterOption(['--no-interaction', '-n'])) {
588
            $input->setInteractive(false);
589
        }
590
591
        if (true === $input->hasParameterOption(['--quiet', '-q'])) {
592
            $output->setVerbosity(Output::VERBOSITY_QUIET);
593
        } elseif ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) {
594
            $output->setVerbosity(Output::VERBOSITY_DEBUG);
595
        } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) {
596
            $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE);
597
        } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) {
598
            $output->setVerbosity(Output::VERBOSITY_VERBOSE);
599
        }
600
    }
601
602
    /**
603
     * 执行指令
604
     * @access protected
605
     * @param  Command $command 指令实例
606
     * @param  Input   $input   输入实例
607
     * @param  Output  $output  输出实例
608
     * @return int
609
     * @throws \Exception
610
     */
611
    protected function doRunCommand(Command $command, Input $input, Output $output)
612
    {
613
        return $command->run($input, $output);
614
    }
615
616
    /**
617
     * 获取指令的基础名称
618
     * @access protected
619
     * @param  Input $input
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
620
     * @return string
621
     */
622
    protected function getCommandName(Input $input): string
623
    {
624
        return $input->getFirstArgument() ?: '';
625
    }
626
627
    /**
628
     * 获取默认输入定义
629
     * @access protected
630
     * @return InputDefinition
631
     */
632
    protected function getDefaultInputDefinition(): InputDefinition
633
    {
634
        return new InputDefinition([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
635
            new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
636
            new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
637
            new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this console version'),
638
            new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
639
            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'),
640
            new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
641
            new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
642
            new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
643
        ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
644
    }
645
646
    /**
647
     * 获取可能的建议
648
     * @access private
649
     * @param  array $abbrevs
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
650
     * @return string
651
     */
652
    private function getAbbreviationSuggestions(array $abbrevs): string
0 ignored issues
show
Coding Style introduced by
Private method name "Console::getAbbreviationSuggestions" must be prefixed with an underscore
Loading history...
653
    {
654
        return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : '');
655
    }
656
657
    /**
658
     * 返回命名空间部分
659
     * @access public
660
     * @param  string $name  指令
661
     * @param  int    $limit 部分的命名空间的最大数量
662
     * @return string
663
     */
664
    public function extractNamespace(string $name, int $limit = 0): string
665
    {
666
        $parts = explode(':', $name);
667
        array_pop($parts);
668
669
        return implode(':', 0 === $limit ? $parts : array_slice($parts, 0, $limit));
670
    }
671
672
    /**
673
     * 查找可替代的建议
674
     * @access private
675
     * @param  string             $name
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
676
     * @param  array|\Traversable $collection
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
677
     * @return array
678
     */
679
    private function findAlternatives(string $name, $collection): array
0 ignored issues
show
Coding Style introduced by
Private method name "Console::findAlternatives" must be prefixed with an underscore
Loading history...
680
    {
681
        $threshold    = 1e3;
682
        $alternatives = [];
683
684
        $collectionParts = [];
685
        foreach ($collection as $item) {
686
            $collectionParts[$item] = explode(':', $item);
687
        }
688
689
        foreach (explode(':', $name) as $i => $subname) {
690
            foreach ($collectionParts as $collectionName => $parts) {
691
                $exists = isset($alternatives[$collectionName]);
692
                if (!isset($parts[$i]) && $exists) {
693
                    $alternatives[$collectionName] += $threshold;
694
                    continue;
695
                } elseif (!isset($parts[$i])) {
696
                    continue;
697
                }
698
699
                $lev = levenshtein($subname, $parts[$i]);
700
                if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) {
701
                    $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
702
                } elseif ($exists) {
703
                    $alternatives[$collectionName] += $threshold;
704
                }
705
            }
706
        }
707
708
        foreach ($collection as $item) {
709
            $lev = levenshtein($name, $item);
710
            if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
711
                $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
712
            }
713
        }
714
715
        $alternatives = array_filter($alternatives, function ($lev) use ($threshold) {
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
716
            return $lev < 2 * $threshold;
717
        });
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
718
        asort($alternatives);
719
720
        return array_keys($alternatives);
721
    }
722
723
    /**
724
     * 返回所有的命名空间
725
     * @access private
726
     * @param  string $name
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
727
     * @return array
728
     */
729
    private function extractAllNamespaces(string $name): array
0 ignored issues
show
Coding Style introduced by
Private method name "Console::extractAllNamespaces" must be prefixed with an underscore
Loading history...
730
    {
731
        $parts      = explode(':', $name, -1);
732
        $namespaces = [];
733
734
        foreach ($parts as $part) {
735
            if (count($namespaces)) {
736
                $namespaces[] = end($namespaces) . ':' . $part;
737
            } else {
738
                $namespaces[] = $part;
739
            }
740
        }
741
742
        return $namespaces;
743
    }
744
745
}
746