Issues (10)

src/Application.php (1 issue)

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