Passed
Push — develop ( add880...26f5dd )
by nguereza
02:36
created

Application::handle()   A

Complexity

Conditions 5
Paths 10

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 10
nop 1
dl 0
loc 22
rs 9.5222
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Platine Console
5
 *
6
 * Platine Console is a powerful library with support of custom
7
 * style to build command line interface applications
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Console
12
 * Copyright (c) 2017-2020 Jitendra Adhikari
13
 *
14
 * Permission is hereby granted, free of charge, to any person obtaining a copy
15
 * of this software and associated documentation files (the "Software"), to deal
16
 * in the Software without restriction, including without limitation the rights
17
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
 * copies of the Software, and to permit persons to whom the Software is
19
 * furnished to do so, subject to the following conditions:
20
 *
21
 * The above copyright notice and this permission notice shall be included in all
22
 * copies or substantial portions of the Software.
23
 *
24
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
 * SOFTWARE.
31
 */
32
33
/**
34
 *  @file Application.php
35
 *
36
 *  The console Application class
37
 *
38
 *  @package    Platine\Console
39
 *  @author Platine Developers Team
40
 *  @copyright  Copyright (c) 2020
41
 *  @license    http://opensource.org/licenses/MIT  MIT License
42
 *  @link   http://www.iacademy.cf
43
 *  @version 1.0.0
44
 *  @filesource
45
 */
46
47
declare(strict_types=1);
48
49
namespace Platine\Console;
50
51
use Platine\Console\Command\Command;
0 ignored issues
show
Bug introduced by
The type Platine\Console\Command\Command was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
52
use Platine\Console\Exception\ConsoleException;
53
use Platine\Console\Exception\InvalidArgumentException;
54
use Platine\Console\IO\Interactor;
55
use Platine\Console\Util\OutputHelper;
56
use Throwable;
57
58
/**
59
 * Class Application
60
 * @package Platine\Console
61
 */
62
class Application
63
{
64
65
    /**
66
     * List of commands
67
     * @var array<Command>
68
     */
69
    protected array $commands = [];
70
71
    /**
72
     * Raw input arguments send to parser
73
     * @var array<int, string>
74
     */
75
    protected array $argv = [];
76
77
    /**
78
     * The commands aliases
79
     * @var array<string, string>
80
     */
81
    protected array $aliases = [];
82
83
    /**
84
     * The name of application
85
     * @var string
86
     */
87
    protected string $name;
88
89
    /**
90
     * The application version
91
     * @var string
92
     */
93
    protected string $version = '';
94
95
    /**
96
     * The application logo using ASCII text
97
     * @var string
98
     */
99
    protected string $logo = '';
100
101
    /**
102
     * The default command to use if none is provided
103
     * @var string
104
     */
105
    protected string $default = '__default__';
106
107
    /**
108
     * The input/output instance
109
     * @var Interactor|null
110
     */
111
    protected ?Interactor $io = null;
112
113
    /**
114
     * The callable to perform exit
115
     * @var callable
116
     */
117
    protected $onExit;
118
119
    public function __construct(
120
        string $name,
121
        string $version = '1.0.0',
122
        ?callable $onExit = null
123
    ) {
124
        $this->name = $name;
125
        $this->version = $version;
126
127
        $this->onExit = $onExit ? $onExit : function (int $exitCode = 0) {
128
            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...
129
        };
130
131
        $this->command('__default__', 'Default command', '', true, true)
132
              ->on([$this, 'showHelp'], 'help');
133
    }
134
135
    /**
136
     * Return the list of command
137
     * @return array<Command>
138
     */
139
    public function getCommands(): array
140
    {
141
        $commands = $this->commands;
142
143
        unset($commands['__default__']);
144
145
        return $commands;
146
    }
147
148
    /**
149
     * Return the command line argument
150
     * @return array<int,string>
151
     */
152
    public function argv(): array
153
    {
154
        return $this->argv;
155
    }
156
157
    /**
158
     * Return the application name
159
     * @return string
160
     */
161
    public function getName(): string
162
    {
163
        return $this->name;
164
    }
165
166
    /**
167
     * Return the application version
168
     * @return string
169
     */
170
    public function getVersion(): string
171
    {
172
        return $this->version;
173
    }
174
175
    /**
176
     * Return the application logo
177
     * @return string
178
     */
179
    public function getLogo(): string
180
    {
181
        return $this->logo;
182
    }
183
184
    /**
185
     * Set application logo
186
     * @param string $logo
187
     * @return $this
188
     */
189
    public function setLogo(string $logo): self
190
    {
191
        $this->logo = $logo;
192
193
        return $this;
194
    }
195
196
197
    /**
198
     * Add a command by it's name, alias
199
     * @param string $name
200
     * @param string $description
201
     * @param string $alias
202
     * @param bool $allowUnknown
203
     * @param bool $default
204
     * @return Command
205
     */
206
    public function command(
207
        string $name,
208
        string $description = '',
209
        string $alias = '',
210
        bool $allowUnknown = false,
211
        bool $default = false
212
    ): Command {
213
        $command = new Command($name, $description, $allowUnknown, $this);
214
215
        $this->addCommand($command, $alias, $default);
216
217
        return $command;
218
    }
219
220
    /**
221
     * Add new command
222
     * @param Command $command
223
     * @param string $alias
224
     * @param bool $default
225
     * @return $this
226
     */
227
    public function addCommand(
228
        Command $command,
229
        string $alias = '',
230
        bool $default = false
231
    ): self {
232
        $name = $command->getName();
233
234
        if (
235
            isset($this->commands[$name])
236
            ||  isset($this->commands[$alias])
237
            ||  isset($this->aliases[$name])
238
            ||  isset($this->aliases[$alias])
239
        ) {
240
            throw new InvalidArgumentException(sprintf(
241
                'Command %s already added',
242
                $name
243
            ));
244
        }
245
246
        if (empty($alias) && !empty($command->getAlias())) {
247
            $alias = $command->getAlias();
248
        }
249
250
        if ($alias) {
251
            $command->setAlias($alias);
252
            $this->aliases[$alias] = $name;
253
        }
254
255
        if ($default) {
256
            $this->default = $name;
257
        }
258
259
        $this->commands[$name] = $command
260
                                    ->setVersion($this->version)
261
                                    ->onExit($this->onExit)
262
                                    ->bind($this);
263
264
        return $this;
265
    }
266
267
    /**
268
     * Gets matching command for given arguments
269
     * @param array<int, string> $argv
270
     * @return Command
271
     */
272
    public function getCommandForArgument(array $argv): Command
273
    {
274
        $argv += [null, null, null];
275
276
        //Command
277
        if (isset($this->commands[$argv[1]])) {
278
            return $this->commands[$argv[1]];
279
        }
280
281
        //Alias
282
        $alias = isset($this->aliases[$argv[1]])
283
                ? $this->aliases[$argv[1]]
284
                : null;
285
286
        if (isset($this->commands[$alias])) {
287
            return $this->commands[$alias];
288
        }
289
290
        //Default command
291
        return $this->commands[$this->default];
292
    }
293
294
    /**
295
     * Return the input/output instance
296
     * @param Interactor|null $io
297
     * @return Interactor
298
     */
299
    public function io(?Interactor $io = null): Interactor
300
    {
301
        if ($io || !$this->io) {
302
            $this->io = $io ? $io : new Interactor();
303
        }
304
305
        return $this->io;
306
    }
307
308
    /**
309
     * Parse the arguments via the matching command
310
     * but don't execute command.
311
     * @param array<int, string> $argv
312
     * @return Command
313
     */
314
    public function parse(array $argv): Command
315
    {
316
        $this->argv = $argv;
317
318
        $command = $this->getCommandForArgument($argv);
319
        $aliases = $this->getAliasesForCommand($command);
320
321
        foreach ($argv as $i => $arg) {
322
            if (in_array($arg, $aliases)) {
323
                unset($argv[$i]);
324
325
                break;
326
            }
327
328
            if ($arg[0] === '-') {
329
                break;
330
            }
331
        }
332
333
        return $command->parse($argv);
334
    }
335
336
    /**
337
     * Handle the request, execute command and call exit handler.
338
     * @param array<int, string> $argv
339
     * @return mixed
340
     */
341
    public function handle(array $argv)
342
    {
343
        if (count($argv) < 2) {
344
            return $this->showHelp();
345
        }
346
347
        $exitCode = 255;
348
349
        try {
350
            $command = $this->parse($argv);
351
            $result = $this->executeCommand($command);
352
353
            $exitCode = is_int($result) ? $result : 0;
354
        } catch (Throwable $ex) {
355
            if ($ex instanceof ConsoleException) {
356
                $this->io()->writer()->red($ex->getMessage(), true);
357
            } else {
358
                $this->outputHelper()->printTrace($ex);
359
            }
360
        }
361
362
        return ($this->onExit)($exitCode);
363
    }
364
365
    /**
366
     * Show help of all commands.
367
     * @return mixed
368
     */
369
    public function showHelp()
370
    {
371
        $writer = $this->io()->writer();
372
373
        $header = sprintf(
374
            '%s, version %s',
375
            $this->name,
376
            $this->version
377
        );
378
379
        $footer = 'Run `<command> --help` for specific help';
380
381
        if ($this->logo) {
382
            $writer->write($this->logo, true);
383
        }
384
385
        $this->outputHelper()
386
                ->showCommandsHelp(
387
                    $this->getCommands(),
388
                    $header,
389
                    $footer
390
                );
391
392
        return ($this->onExit)();
393
    }
394
395
    /**
396
     * Return the list of alias for given command
397
     * @param Command $command
398
     * @return array<int, string>
399
     */
400
    public function getAliasesForCommand(Command $command): array
401
    {
402
        $name = $command->getName();
403
        $aliases = [$name];
404
405
        foreach ($this->aliases as $alias => $commandName) {
406
            if (in_array($name, [$alias, $commandName])) {
407
                $aliases[] = $alias;
408
                $aliases[] = $commandName;
409
            }
410
        }
411
412
        return $aliases;
413
    }
414
415
    /**
416
     * Execute the given command and return the result
417
     * @param Command $command
418
     * @return mixed
419
     */
420
    protected function executeCommand(Command $command)
421
    {
422
        if ($command->getName() === '__default__') {
423
            return $this->showCommandNotFound();
424
        }
425
426
        // Let the command collect more data
427
        // (if missing or needs confirmation)
428
        $command->interact($this->io());
429
430
        return $command->execute();
431
    }
432
433
    /**
434
     * Return the output helper instance
435
     * @return OutputHelper
436
     */
437
    protected function outputHelper(): OutputHelper
438
    {
439
        $writer = $this->io()->writer();
440
441
        return new OutputHelper($writer);
442
    }
443
444
    /**
445
     * Command not found handler.
446
     * @return mixed
447
     */
448
    protected function showCommandNotFound()
449
    {
450
        $available = array_keys($this->getCommands() + $this->aliases);
451
452
        $this->outputHelper()
453
                ->showCommandNotFound($this->argv[1], $available);
454
455
        return ($this->onExit)(127);
456
    }
457
}
458