Passed
Push — 5.2 ( 34572a...8f25db )
by
unknown
02:59
created

Console::addCommand()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 9
nop 2
dl 0
loc 31
rs 9.1111
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
class Console
1 ignored issue
show
Coding Style introduced by
Missing class doc comment
Loading history...
43
{
44
45
    protected $app;
46
47
    /** @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...
48
    protected $commands = [];
49
50
    protected $wantHelps = false;
51
52
    protected $catchExceptions = true;
53
    protected $autoExit        = true;
54
    protected $definition;
55
    protected $defaultCommand  = 'list';
56
57
    protected $defaultCommands = [
58
        'help'             => Help::class,
59
        'list'             => Lists::class,
60
        'build'            => Build::class,
61
        'clear'            => Clear::class,
62
        'make:command'     => MakeCommand::class,
63
        'make:controller'  => Controller::class,
64
        'make:model'       => Model::class,
65
        'make:middleware'  => Middleware::class,
66
        'make:validate'    => Validate::class,
67
        'make:event'       => Event::class,
68
        'make:listener'    => Listener::class,
69
        'make:subscribe'   => Subscribe::class,
70
        'optimize:config'  => Config::class,
71
        'optimize:schema'  => Schema::class,
72
        'optimize:route'   => Route::class,
73
        'optimize:facade'  => Facade::class,
74
        'run'              => RunServer::class,
75
        'version'          => Version::class,
76
        'route:list'       => RouteList::class,
77
        'service:discover' => ServiceDiscover::class
78
    ];
79
80
    /**
81
     * 初始化器
82
     * @var array
83
     */
84
    protected static $initializers = [];
85
86
    public function __construct(App $app)
0 ignored issues
show
Coding Style introduced by
Missing function doc comment
Loading history...
87
    {
88
        $this->app = $app;
89
90
        $user = $this->app->config->get('console.user');
91
92
        if ($user) {
93
            $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

93
            $this->setUser(/** @scrutinizer ignore-type */ $user);
Loading history...
94
        }
95
96
        $this->definition = $this->getDefaultInputDefinition();
97
98
        //加载指令
99
        $this->loadCommands();
100
101
        $this->initialize();
102
    }
103
104
    /**
105
     * 添加初始化器
106
     * @param Closure $callback
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
107
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
108
    public static function init(Closure $callback)
109
    {
110
        static::$initializers[] = $callback;
111
    }
112
113
    /**
114
     * 清空初始化器
115
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
116
    public static function flushInit()
117
    {
118
        static::$initializers = [];
119
    }
120
121
    /**
122
     * 初始化
123
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
124
    protected function initialize(): void
125
    {
126
        foreach (static::$initializers as $initialize) {
127
            $initialize($this);
128
        }
129
    }
130
131
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $user should have a doc-comment as per coding-style.
Loading history...
132
     * 设置执行用户
133
     * @param $user
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
134
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
135
    protected function setUser(string $user): void
136
    {
137
        if (function_exists('posix_getpwnam')) {
138
            $user = posix_getpwnam($user);
139
140
            if (!empty($user)) {
141
                posix_setuid($user['uid']);
142
                posix_setgid($user['gid']);
143
            }
144
        }
145
    }
146
147
    /**
148
     * 加载指令
149
     * @access protected
150
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
151
    protected function loadCommands(): void
152
    {
153
        //加载默认指令
154
        $this->addCommands($this->defaultCommands);
155
156
        $commands = $this->app->config->get('console.commands', []);
157
158
        $this->addCommands($commands);
159
    }
160
161
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
162
     * @access public
163
     * @param  string $command
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
164
     * @param  array  $parameters
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
165
     * @param  string $driver
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
166
     * @return Output|Buffer
167
     */
168
    public function call(string $command, array $parameters = [], string $driver = 'buffer')
169
    {
170
        array_unshift($parameters, $command);
171
172
        $input  = new Input($parameters);
173
        $output = new Output($driver);
174
175
        $this->setCatchExceptions(false);
176
        $this->find($command)->run($input, $output);
177
178
        return $output;
179
    }
180
181
    /**
182
     * 执行当前的指令
183
     * @access public
184
     * @return int
185
     * @throws \Exception
186
     * @api
187
     */
188
    public function run()
189
    {
190
        $input  = new Input();
191
        $output = new Output();
192
193
        $this->configureIO($input, $output);
194
195
        try {
196
            $exitCode = $this->doRun($input, $output);
197
        } catch (\Exception $e) {
198
            if (!$this->catchExceptions) {
199
                throw $e;
200
            }
201
202
            $output->renderException($e);
203
204
            $exitCode = $e->getCode();
205
            if (is_numeric($exitCode)) {
206
                $exitCode = (int) $exitCode;
207
                if (0 === $exitCode) {
208
                    $exitCode = 1;
209
                }
210
            } else {
211
                $exitCode = 1;
212
            }
213
        }
214
215
        if ($this->autoExit) {
216
            if ($exitCode > 255) {
217
                $exitCode = 255;
218
            }
219
220
            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...
221
        }
222
223
        return $exitCode;
224
    }
225
226
    /**
227
     * 执行指令
228
     * @access public
229
     * @param  Input  $input
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
230
     * @param  Output $output
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
231
     * @return int
232
     */
233
    public function doRun(Input $input, Output $output)
234
    {
235
        if (true === $input->hasParameterOption(['--version', '-V'])) {
236
            $output->writeln($this->getLongVersion());
237
238
            return 0;
239
        }
240
241
        $name = $this->getCommandName($input);
242
243
        if (true === $input->hasParameterOption(['--help', '-h'])) {
244
            if (!$name) {
245
                $name  = 'help';
246
                $input = new Input(['help']);
247
            } else {
248
                $this->wantHelps = true;
249
            }
250
        }
251
252
        if (!$name) {
253
            $name  = $this->defaultCommand;
254
            $input = new Input([$this->defaultCommand]);
255
        }
256
257
        $command = $this->find($name);
258
259
        return $this->doRunCommand($command, $input, $output);
260
    }
261
262
    /**
263
     * 设置输入参数定义
264
     * @access public
265
     * @param  InputDefinition $definition
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
266
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
267
    public function setDefinition(InputDefinition $definition)
268
    {
269
        $this->definition = $definition;
270
    }
271
272
    /**
273
     * 获取输入参数定义
274
     * @access public
275
     * @return InputDefinition The InputDefinition instance
276
     */
277
    public function getDefinition()
278
    {
279
        return $this->definition;
280
    }
281
282
    /**
283
     * Gets the help message.
284
     * @access public
285
     * @return string A help message.
286
     */
287
    public function getHelp()
288
    {
289
        return $this->getLongVersion();
290
    }
291
292
    /**
293
     * 是否捕获异常
294
     * @access public
295
     * @param  bool $boolean
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
296
     * @api
297
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
298
    public function setCatchExceptions(bool $boolean)
299
    {
300
        $this->catchExceptions = $boolean;
301
    }
302
303
    /**
304
     * 是否自动退出
305
     * @access public
306
     * @param  bool $boolean
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
307
     * @api
308
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
309
    public function setAutoExit(bool $boolean)
310
    {
311
        $this->autoExit = $boolean;
312
    }
313
314
    /**
315
     * 获取完整的版本号
316
     * @access public
317
     * @return string
318
     */
319
    public function getLongVersion()
320
    {
321
        if ('UNKNOWN' !== $this->app->getName() && 'UNKNOWN' !== $this->app->version()) {
322
            return sprintf('<info>%s</info> version <comment>%s</comment>', $this->app->getName(), $this->app->version());
323
        }
324
325
        return '<info>Console Tool</info>';
326
    }
327
328
    /**
329
     * 注册一个指令 (便于动态创建指令)
330
     * @access public
331
     * @param  string $name 指令名
332
     * @return Command
333
     */
334
    public function register(string $name)
335
    {
336
        return $this->addCommand(new Command($name));
337
    }
338
339
    /**
340
     * 添加指令集
341
     * @access public
342
     * @param  array $commands
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
343
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
344
    public function addCommands(array $commands)
345
    {
346
        foreach ($commands as $key => $command) {
347
            if (is_subclass_of($command, "\\think\\console\\Command")) {
348
                // 注册指令
349
                $this->addCommand($command, is_numeric($key) ? '' : $key);
350
            }
351
        }
352
    }
353
354
    /**
355
     * 添加一个指令
356
     * @access public
357
     * @param  string|Command $command 指令对象或者指令类名
358
     * @param  string         $name    指令名 留空则自动获取
359
     * @return Command|null
360
     */
361
    public function addCommand($command, string $name = '')
362
    {
363
        if ($name) {
364
            $this->commands[$name] = $command;
365
            return;
366
        }
367
368
        if (is_string($command)) {
369
            $command = new $command();
370
        }
371
372
        $command->setConsole($this);
373
374
        if (!$command->isEnabled()) {
375
            $command->setConsole(null);
376
            return;
377
        }
378
379
        $command->setApp($this->app);
380
381
        if (null === $command->getDefinition()) {
382
            throw new \LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command)));
383
        }
384
385
        $this->commands[$command->getName()] = $command;
386
387
        foreach ($command->getAliases() as $alias) {
388
            $this->commands[$alias] = $command;
389
        }
390
391
        return $command;
392
    }
393
394
    /**
395
     * 获取指令
396
     * @access public
397
     * @param  string $name 指令名称
398
     * @return Command
399
     * @throws \InvalidArgumentException
400
     */
401
    public function getCommand(string $name)
402
    {
403
        if (!isset($this->commands[$name])) {
404
            throw new \InvalidArgumentException(sprintf('The command "%s" does not exist.', $name));
405
        }
406
407
        $command = $this->commands[$name];
408
409
        if (is_string($command)) {
0 ignored issues
show
introduced by
The condition is_string($command) is always false.
Loading history...
410
            $command = new $command();
411
        }
412
413
        $command->setConsole($this);
414
415
        if ($this->wantHelps) {
416
            $this->wantHelps = false;
417
418
            /** @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...
419
            $helpCommand = $this->getCommand('help');
420
            $helpCommand->setCommand($command);
421
422
            return $helpCommand;
423
        }
424
425
        return $command;
426
    }
427
428
    /**
429
     * 某个指令是否存在
430
     * @access public
431
     * @param  string $name 指令名称
432
     * @return bool
433
     */
434
    public function hasCommand(string $name)
435
    {
436
        return isset($this->commands[$name]);
437
    }
438
439
    /**
440
     * 获取所有的命名空间
441
     * @access public
442
     * @return array
443
     */
444
    public function getNamespaces()
445
    {
446
        $namespaces = [];
447
        foreach ($this->commands as $command) {
448
            if (is_string($command)) {
449
                $namespaces[] = $command;
450
            } else {
451
                $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName()));
452
453
                foreach ($command->getAliases() as $alias) {
454
                    $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias));
455
                }
456
            }
457
        }
458
459
        return array_values(array_unique(array_filter($namespaces)));
460
    }
461
462
    /**
463
     * 查找注册命名空间中的名称或缩写。
464
     * @access public
465
     * @param  string $namespace
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
466
     * @return string
467
     * @throws \InvalidArgumentException
468
     */
469
    public function findNamespace(string $namespace)
470
    {
471
        $allNamespaces = $this->getNamespaces();
472
        $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...
473
            return preg_quote($matches[1]) . '[^:]*';
474
        }, $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...
475
        $namespaces    = preg_grep('{^' . $expr . '}', $allNamespaces);
476
477
        if (empty($namespaces)) {
478
            $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
479
480
            if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
481
                if (1 == count($alternatives)) {
482
                    $message .= "\n\nDid you mean this?\n    ";
483
                } else {
484
                    $message .= "\n\nDid you mean one of these?\n    ";
485
                }
486
487
                $message .= implode("\n    ", $alternatives);
488
            }
489
490
            throw new \InvalidArgumentException($message);
491
        }
492
493
        $exact = in_array($namespace, $namespaces, true);
494
        if (count($namespaces) > 1 && !$exact) {
495
            throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))));
496
        }
497
498
        return $exact ? $namespace : reset($namespaces);
499
    }
500
501
    /**
502
     * 查找指令
503
     * @access public
504
     * @param  string $name 名称或者别名
505
     * @return Command
506
     * @throws \InvalidArgumentException
507
     */
508
    public function find(string $name)
509
    {
510
        $allCommands = array_keys($this->commands);
511
512
        $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...
513
            return preg_quote($matches[1]) . '[^:]*';
514
        }, $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...
515
516
        $commands = preg_grep('{^' . $expr . '}', $allCommands);
517
518
        if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) {
519
            if (false !== $pos = strrpos($name, ':')) {
520
                $this->findNamespace(substr($name, 0, $pos));
521
            }
522
523
            $message = sprintf('Command "%s" is not defined.', $name);
524
525
            if ($alternatives = $this->findAlternatives($name, $allCommands)) {
526
                if (1 == count($alternatives)) {
527
                    $message .= "\n\nDid you mean this?\n    ";
528
                } else {
529
                    $message .= "\n\nDid you mean one of these?\n    ";
530
                }
531
                $message .= implode("\n    ", $alternatives);
532
            }
533
534
            throw new \InvalidArgumentException($message);
535
        }
536
537
        $exact = in_array($name, $commands, true);
538
        if (count($commands) > 1 && !$exact) {
539
            $suggestions = $this->getAbbreviationSuggestions(array_values($commands));
540
541
            throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions));
542
        }
543
544
        return $this->getCommand($exact ? $name : reset($commands));
545
    }
546
547
    /**
548
     * 获取所有的指令
549
     * @access public
550
     * @param  string $namespace 命名空间
551
     * @return Command[]
552
     * @api
553
     */
554
    public function all(string $namespace = null)
555
    {
556
        if (null === $namespace) {
557
            return $this->commands;
558
        }
559
560
        $commands = [];
561
        foreach ($this->commands as $name => $command) {
562
            if ($this->extractNamespace($name, substr_count($namespace, ':') + 1) === $namespace) {
563
                $commands[$name] = $command;
564
            }
565
        }
566
567
        return $commands;
568
    }
569
570
    /**
571
     * 获取可能的指令名
572
     * @access public
573
     * @param  array $names
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
574
     * @return array
575
     */
576
    public static function getAbbreviations(array $names)
577
    {
578
        $abbrevs = [];
579
        foreach ($names as $name) {
580
            for ($len = strlen($name); $len > 0; --$len) {
581
                $abbrev             = substr($name, 0, $len);
582
                $abbrevs[$abbrev][] = $name;
583
            }
584
        }
585
586
        return $abbrevs;
587
    }
588
589
    /**
590
     * 配置基于用户的参数和选项的输入和输出实例。
591
     * @access protected
592
     * @param  Input  $input  输入实例
593
     * @param  Output $output 输出实例
594
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
595
    protected function configureIO(Input $input, Output $output)
596
    {
597
        if (true === $input->hasParameterOption(['--ansi'])) {
598
            $output->setDecorated(true);
599
        } elseif (true === $input->hasParameterOption(['--no-ansi'])) {
600
            $output->setDecorated(false);
601
        }
602
603
        if (true === $input->hasParameterOption(['--no-interaction', '-n'])) {
604
            $input->setInteractive(false);
605
        }
606
607
        if (true === $input->hasParameterOption(['--quiet', '-q'])) {
608
            $output->setVerbosity(Output::VERBOSITY_QUIET);
609
        } elseif ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) {
610
            $output->setVerbosity(Output::VERBOSITY_DEBUG);
611
        } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) {
612
            $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE);
613
        } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) {
614
            $output->setVerbosity(Output::VERBOSITY_VERBOSE);
615
        }
616
    }
617
618
    /**
619
     * 执行指令
620
     * @access protected
621
     * @param  Command $command 指令实例
622
     * @param  Input   $input   输入实例
623
     * @param  Output  $output  输出实例
624
     * @return int
625
     * @throws \Exception
626
     */
627
    protected function doRunCommand(Command $command, Input $input, Output $output)
628
    {
629
        return $command->run($input, $output);
630
    }
631
632
    /**
633
     * 获取指令的基础名称
634
     * @access protected
635
     * @param  Input $input
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
636
     * @return string
637
     */
638
    protected function getCommandName(Input $input)
639
    {
640
        return $input->getFirstArgument();
641
    }
642
643
    /**
644
     * 获取默认输入定义
645
     * @access protected
646
     * @return InputDefinition
647
     */
648
    protected function getDefaultInputDefinition()
649
    {
650
        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...
651
            new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
652
            new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
653
            new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this console version'),
654
            new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
655
            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'),
656
            new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
657
            new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
658
            new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
659
        ]);
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...
660
    }
661
662
    /**
663
     * 获取可能的建议
664
     * @access private
665
     * @param  array $abbrevs
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
666
     * @return string
667
     */
668
    private function getAbbreviationSuggestions(array $abbrevs)
0 ignored issues
show
Coding Style introduced by
Private method name "Console::getAbbreviationSuggestions" must be prefixed with an underscore
Loading history...
669
    {
670
        return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : '');
671
    }
672
673
    /**
674
     * 返回命名空间部分
675
     * @access public
676
     * @param  string $name  指令
677
     * @param  int    $limit 部分的命名空间的最大数量
678
     * @return string
679
     */
680
    public function extractNamespace(string $name, int $limit = 0)
681
    {
682
        $parts = explode(':', $name);
683
        array_pop($parts);
684
685
        return implode(':', 0 === $limit ? $parts : array_slice($parts, 0, $limit));
686
    }
687
688
    /**
689
     * 查找可替代的建议
690
     * @access private
691
     * @param  string             $name
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
692
     * @param  array|\Traversable $collection
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
693
     * @return array
694
     */
695
    private function findAlternatives(string $name, $collection)
0 ignored issues
show
Coding Style introduced by
Private method name "Console::findAlternatives" must be prefixed with an underscore
Loading history...
696
    {
697
        $threshold    = 1e3;
698
        $alternatives = [];
699
700
        $collectionParts = [];
701
        foreach ($collection as $item) {
702
            $collectionParts[$item] = explode(':', $item);
703
        }
704
705
        foreach (explode(':', $name) as $i => $subname) {
706
            foreach ($collectionParts as $collectionName => $parts) {
707
                $exists = isset($alternatives[$collectionName]);
708
                if (!isset($parts[$i]) && $exists) {
709
                    $alternatives[$collectionName] += $threshold;
710
                    continue;
711
                } elseif (!isset($parts[$i])) {
712
                    continue;
713
                }
714
715
                $lev = levenshtein($subname, $parts[$i]);
716
                if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) {
717
                    $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
718
                } elseif ($exists) {
719
                    $alternatives[$collectionName] += $threshold;
720
                }
721
            }
722
        }
723
724
        foreach ($collection as $item) {
725
            $lev = levenshtein($name, $item);
726
            if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
727
                $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
728
            }
729
        }
730
731
        $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...
732
            return $lev < 2 * $threshold;
733
        });
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...
734
        asort($alternatives);
735
736
        return array_keys($alternatives);
737
    }
738
739
    /**
740
     * 返回所有的命名空间
741
     * @access private
742
     * @param  string $name
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
743
     * @return array
744
     */
745
    private function extractAllNamespaces(string $name)
0 ignored issues
show
Coding Style introduced by
Private method name "Console::extractAllNamespaces" must be prefixed with an underscore
Loading history...
746
    {
747
        $parts      = explode(':', $name, -1);
748
        $namespaces = [];
749
750
        foreach ($parts as $part) {
751
            if (count($namespaces)) {
752
                $namespaces[] = end($namespaces) . ':' . $part;
753
            } else {
754
                $namespaces[] = $part;
755
            }
756
        }
757
758
        return $namespaces;
759
    }
760
761
}
762