Passed
Push — master ( 763e25...c31763 )
by Jitendra
01:39
created

Application   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 356
Duplicated Lines 0 %

Importance

Changes 12
Bugs 2 Features 2
Metric Value
eloc 101
c 12
b 2
f 2
dl 0
loc 356
rs 9.28
wmc 39

18 Methods

Rating   Name   Duplication   Size   Complexity  
A showHelp() 0 13 2
A argv() 0 3 1
A version() 0 3 1
A parse() 0 21 4
A name() 0 3 1
A add() 0 20 4
A handle() 0 17 4
A __construct() 0 12 1
A outputHelper() 0 5 1
A doAction() 0 23 5
A command() 0 12 1
A io() 0 11 4
A commandFor() 0 11 1
A commands() 0 7 1
A logo() 0 9 2
A aliasesFor() 0 12 3
A getActionParameters() 0 7 2
A notFound() 0 6 1
1
<?php
2
3
/*
4
 * This file is part of the PHP-CLI package.
5
 *
6
 * (c) Jitendra Adhikari <[email protected]>
7
 *     <https://github.com/adhocore>
8
 *
9
 * Licensed under MIT license.
10
 */
11
12
namespace Ahc\Cli;
13
14
use Ahc\Cli\Exception\InvalidArgumentException;
15
use Ahc\Cli\Helper\OutputHelper;
16
use Ahc\Cli\Input\Command;
17
use Ahc\Cli\IO\Interactor;
18
19
/**
20
 * A cli application.
21
 *
22
 * @author  Jitendra Adhikari <[email protected]>
23
 * @license MIT
24
 *
25
 * @link    https://github.com/adhocore/cli
26
 */
27
class Application
28
{
29
    /** @var Command[] */
30
    protected $commands = [];
31
32
    /** @var array Raw argv sent to parse() */
33
    protected $argv = [];
34
35
    /** @var array Command aliases [alias => cmd] */
36
    protected $aliases = [];
37
38
    /** @var string */
39
    protected $name;
40
41
    /** @var string App version */
42
    protected $version = '';
43
44
    /** @var string Ascii art logo */
45
    protected $logo = '';
46
47
    protected $default = '__default__';
48
49
    /** @var Interactor */
50
    protected $io;
51
52
    /** @var callable The callable to perform exit */
53
    protected $onExit;
54
55
    public function __construct(string $name, string $version = '0.0.1', callable $onExit = null)
56
    {
57
        $this->name    = $name;
58
        $this->version = $version;
59
60
        // @codeCoverageIgnoreStart
61
        $this->onExit = $onExit ?? function ($exitCode = 0) {
62
            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...
63
        };
64
        // @codeCoverageIgnoreEnd
65
66
        $this->command('__default__', 'Default command', '', true)->on([$this, 'showHelp'], 'help');
67
    }
68
69
    /**
70
     * Get the name.
71
     *
72
     * @return string
73
     */
74
    public function name(): string
75
    {
76
        return $this->name;
77
    }
78
79
    /**
80
     * Get the version.
81
     *
82
     * @return string
83
     */
84
    public function version(): string
85
    {
86
        return $this->version;
87
    }
88
89
    /**
90
     * Get the commands.
91
     *
92
     * @return Command[]
93
     */
94
    public function commands(): array
95
    {
96
        $commands = $this->commands;
97
98
        unset($commands['__default__']);
99
100
        return $commands;
101
    }
102
103
    /**
104
     * Get the raw argv.
105
     *
106
     * @return array
107
     */
108
    public function argv(): array
109
    {
110
        return $this->argv;
111
    }
112
113
    /**
114
     * Sets or gets the ASCII art logo.
115
     *
116
     * @param string|null $logo
117
     *
118
     * @return string|self
119
     */
120
    public function logo(string $logo = null)
121
    {
122
        if (\func_num_args() === 0) {
123
            return $this->logo;
124
        }
125
126
        $this->logo = $logo;
127
128
        return $this;
129
    }
130
131
    /**
132
     * Add a command by its name desc alias etc.
133
     *
134
     * @param string $name
135
     * @param string $desc
136
     * @param string $alias
137
     * @param bool   $allowUnknown
138
     * @param bool   $default
139
     *
140
     * @return Command
141
     */
142
    public function command(
143
        string $name,
144
        string $desc = '',
145
        string $alias = '',
146
        bool $allowUnknown = false,
147
        bool $default = false
148
    ): Command {
149
        $command = new Command($name, $desc, $allowUnknown, $this);
150
151
        $this->add($command, $alias, $default);
152
153
        return $command;
154
    }
155
156
    /**
157
     * Add a prepred command.
158
     *
159
     * @param Command $command
160
     * @param string  $alias
161
     * @param bool    $default
162
     *
163
     * @return self
164
     */
165
    public function add(Command $command, string $alias = '', bool $default = false): self
166
    {
167
        $name = $command->name();
168
169
        if ($this->commands[$name] ?? $this->aliases[$name] ?? $this->commands[$alias] ?? $this->aliases[$alias] ?? null) {
170
            throw new InvalidArgumentException(\sprintf('Command "%s" already added', $name));
171
        }
172
173
        if ($alias) {
174
            $command->alias($alias);
175
            $this->aliases[$alias] = $name;
176
        }
177
178
        if ($default) {
179
            $this->default = $name;
180
        }
181
182
        $this->commands[$name] = $command->version($this->version)->onExit($this->onExit)->bind($this);
183
184
        return $this;
185
    }
186
187
    /**
188
     * Gets matching command for given argv.
189
     *
190
     * @param array $argv
191
     *
192
     * @return Command
193
     */
194
    public function commandFor(array $argv): Command
195
    {
196
        $argv += [null, null, null];
197
198
        return
199
             // cmd
200
            $this->commands[$argv[1]]
201
            // cmd alias
202
            ?? $this->commands[$this->aliases[$argv[1]] ?? null]
203
            // default.
204
            ?? $this->commands[$this->default];
205
    }
206
207
    /**
208
     * Gets or sets io.
209
     *
210
     * @param Interactor|null $io
211
     *
212
     * @return Interactor|self
213
     */
214
    public function io(Interactor $io = null)
215
    {
216
        if ($io || !$this->io) {
217
            $this->io = $io ?? new Interactor;
218
        }
219
220
        if (\func_num_args() === 0) {
221
            return $this->io;
222
        }
223
224
        return $this;
225
    }
226
227
    /**
228
     * Parse the arguments via the matching command but dont execute action..
229
     *
230
     * @param array $argv Cli arguments/options.
231
     *
232
     * @return Command The matched and parsed command (or default)
233
     */
234
    public function parse(array $argv): Command
235
    {
236
        $this->argv = $argv;
237
238
        $command = $this->commandFor($argv);
239
        $aliases = $this->aliasesFor($command);
240
241
        // Eat the cmd name!
242
        foreach ($argv as $i => $arg) {
243
            if (\in_array($arg, $aliases)) {
244
                unset($argv[$i]);
245
246
                break;
247
            }
248
249
            if ($arg[0] === '-') {
250
                break;
251
            }
252
        }
253
254
        return $command->parse($argv);
255
    }
256
257
    /**
258
     * Handle the request, invoke action and call exit handler.
259
     *
260
     * @param array $argv
261
     *
262
     * @return mixed
263
     */
264
    public function handle(array $argv)
265
    {
266
        if (\count($argv) < 2) {
267
            return $this->showHelp();
268
        }
269
270
        $exitCode = 255;
271
272
        try {
273
            $command  = $this->parse($argv);
274
            $result   = $this->doAction($command);
275
            $exitCode = \is_int($result) ? $result : 0;
276
        } catch (\Throwable $e) {
277
            $this->outputHelper()->printTrace($e);
278
        }
279
280
        return ($this->onExit)($exitCode);
281
    }
282
283
    /**
284
     * Get aliases for given command.
285
     *
286
     * @param Command $command
287
     *
288
     * @return array
289
     */
290
    protected function aliasesFor(Command $command): array
291
    {
292
        $aliases = [$name = $command->name()];
293
294
        foreach ($this->aliases as $alias => $command) {
295
            if (\in_array($name, [$alias, $command])) {
296
                $aliases[] = $alias;
297
                $aliases[] = $command;
298
            }
299
        }
300
301
        return $aliases;
302
    }
303
304
    /**
305
     * Show help of all commands.
306
     *
307
     * @return mixed
308
     */
309
    public function showHelp()
310
    {
311
        $writer = $this->io()->writer();
312
        $header = "{$this->name}, version {$this->version}";
313
        $footer = 'Run `<command> --help` for specific help';
314
315
        if ($this->logo) {
316
            $writer->write($this->logo, true);
317
        }
318
319
        $this->outputHelper()->showCommandsHelp($this->commands(), $header, $footer);
320
321
        return ($this->onExit)();
322
    }
323
324
    protected function outputHelper(): OutputHelper
325
    {
326
        $writer = $this->io()->writer();
327
328
        return new OutputHelper($writer);
329
    }
330
331
    /**
332
     * Invoke command action.
333
     *
334
     * @param Command $command
335
     *
336
     * @return mixed
337
     */
338
    protected function doAction(Command $command)
339
    {
340
        if ($command->name() === '__default__') {
341
            return $this->notFound();
342
        }
343
344
        // Let the command collect more data (if missing or needs confirmation)
345
        $command->interact($this->io());
346
347
        if (!$command->action() && !\method_exists($command, 'execute')) {
348
            return;
349
        }
350
351
        $params = [];
352
        $values = $command->values();
353
        // We prioritize action to be in line with commander.js!
354
        $action = $command->action() ?? [$command, 'execute'];
355
356
        foreach ($this->getActionParameters($action) as $param) {
357
            $params[] = $values[$param->getName()] ?? null;
358
        }
359
360
        return $action(...$params);
361
    }
362
363
    /**
364
     * Command not found handler.
365
     *
366
     * @return mixed
367
     */
368
    protected function notFound()
369
    {
370
        $available = \array_keys($this->commands() + $this->aliases);
371
        $this->outputHelper()->showCommandNotFound($this->argv[1], $available);
372
373
        return ($this->onExit)(127);
374
    }
375
376
    protected function getActionParameters(callable $action): array
377
    {
378
        $reflex = \is_array($action)
379
            ? (new \ReflectionClass($action[0]))->getMethod($action[1])
380
            : new \ReflectionFunction($action);
381
382
        return $reflex->getParameters();
383
    }
384
}
385