Issues (10)

src/Command/Command.php (3 issues)

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 Command.php
35
 *
36
 *  The Command class
37
 *
38
 *  @package    Platine\Console\Command
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\Command;
50
51
use Platine\Console\Application;
52
use Platine\Console\Exception\InvalidParameterException;
53
use Platine\Console\Exception\RuntimeException;
54
use Platine\Console\Input\Argument;
55
use Platine\Console\Input\Option;
56
use Platine\Console\Input\Parser;
57
use Platine\Console\Input\Reader;
58
use Platine\Console\IO\Interactor;
59
use Platine\Console\Output\Writer;
60
use Platine\Console\Util\Helper;
61
use Platine\Console\Util\OutputHelper;
62
63
/**
64
 * @class Command
65
 * @package Platine\Console\Command
66
 */
67
class Command extends Parser
68
{
69
    /**
70
     * The command version
71
     * @var string
72
     */
73
    protected string $version = '';
74
75
    /**
76
     * The command name
77
     * @var string
78
     */
79
    protected string $name;
80
81
    /**
82
     * The command description
83
     * @var string
84
     */
85
    protected string $description = '';
86
87
    /**
88
     * The command usage example
89
     * @var string
90
     */
91
    protected string $usage = '';
92
93
    /**
94
     * The command alias
95
     * @var string
96
     */
97
    protected string $alias = '';
98
99
    /**
100
     * The Application instance
101
     * @var Application|null
102
     */
103
    protected ?Application $app = null;
104
105
    /**
106
     * The options events
107
     * @var array<callable>
108
     */
109
    protected array $events = [];
110
111
    /**
112
     * Whether to allow unknown (not registered) options
113
     * @var bool
114
     */
115
    protected bool $allowUnknown = false;
116
117
    /**
118
     * If the last seen argument was variadic
119
     * @var bool
120
     */
121
    protected bool $argVariadic = false;
122
123
    /**
124
     * The verbosity level
125
     * @var int
126
     */
127
    protected int $verbosity = 0;
128
129
    /**
130
     * Create new instance
131
     * @param string $name
132
     * @param string $description
133
     * @param bool $allowUnknown
134
     * @param Application|null $app
135
     */
136
    public function __construct(
137
        string $name,
138
        string $description = '',
139
        bool $allowUnknown = false,
140
        ?Application $app = null
141
    ) {
142
        $this->name = $name;
143
        $this->description = $description;
144
        $this->allowUnknown = $allowUnknown;
145
        $this->app = $app;
146
147
        $this->defaults();
148
    }
149
150
    /**
151
     * Return the version of this command
152
     * @return string
153
     */
154
    public function getVersion(): string
155
    {
156
        return $this->version;
157
    }
158
159
    /**
160
     * Set the command version
161
     * @param string $version
162
     * @return $this
163
     */
164
    public function setVersion(string $version): self
165
    {
166
        $this->version = $version;
167
168
        return $this;
169
    }
170
171
    /**
172
     * Return the command name
173
     * @return string
174
     */
175
    public function getName(): string
176
    {
177
        return $this->name;
178
    }
179
180
    /**
181
     * Set the command name
182
     * @param string $name
183
     * @return $this
184
     */
185
    public function setName(string $name): self
186
    {
187
        $this->name = $name;
188
189
        return $this;
190
    }
191
192
    /**
193
     * Return the command description
194
     * @return string
195
     */
196
    public function getDescription(): string
197
    {
198
        return $this->description;
199
    }
200
201
    /**
202
     * Set the command description
203
     * @param string $description
204
     * @return $this
205
     */
206
    public function setDescription(string $description): self
207
    {
208
        $this->description = $description;
209
210
        return $this;
211
    }
212
213
    /**
214
     * Return the application instance
215
     * @return Application|null
216
     */
217
    public function getApp(): ?Application
218
    {
219
        return $this->app;
220
    }
221
222
    /**
223
     * Bind to given application
224
     * @param Application|null $app
225
     * @return $this
226
     */
227
    public function bind(?Application $app): self
228
    {
229
        $this->app = $app;
230
231
        return $this;
232
    }
233
234
    /**
235
     * Return the command usage
236
     * @return string
237
     */
238
    public function getUsage(): string
239
    {
240
        return $this->usage;
241
    }
242
243
    /**
244
     * Set the command usage
245
     * @param string $usage
246
     * @return $this
247
     */
248
    public function setUsage(string $usage): self
249
    {
250
        $this->usage = $usage;
251
252
        return $this;
253
    }
254
255
    /**
256
     * Return the command alias
257
     * @return string
258
     */
259
    public function getAlias(): string
260
    {
261
        return $this->alias;
262
    }
263
264
    /**
265
     * Set the command alias
266
     * @param string $alias
267
     * @return $this
268
     */
269
    public function setAlias(string $alias): self
270
    {
271
        $this->alias = $alias;
272
273
        return $this;
274
    }
275
276
    /**
277
     * Sets event handler for last (or given) option.
278
     * @param callable $callback
279
     * @param string|null $option
280
     * @return $this
281
     */
282
    public function on(callable $callback, ?string $option = null): self
283
    {
284
        $names = array_keys($this->options());
285
        $this->events[$option ? $option : end($names)] = $callback;
286
287
        return $this;
288
    }
289
290
    /**
291
     * Register exit handler.
292
     * @param callable $callback
293
     * @return $this
294
     */
295
    public function onExit(callable $callback): self
296
    {
297
        $this->events['_exit'] = $callback;
298
299
        return $this;
300
    }
301
302
    /**
303
     * Add command option
304
     * @param string $raw
305
     * @param string $description
306
     * @param mixed $default
307
     * @param bool $required
308
     * @param bool $variadic
309
     * @param callable|null $filter
310
     * @return $this
311
     */
312
    public function addOption(
313
        string $raw,
314
        string $description,
315
        mixed $default = null,
316
        bool $required = false,
317
        bool $variadic = false,
318
        ?callable $filter = null
319
    ): self {
320
        $option = new Option(
321
            $raw,
322
            $description,
323
            $default,
324
            $required,
325
            $variadic,
326
            $filter
327
        );
328
        $this->register($option);
329
330
        return $this;
331
    }
332
333
    /**
334
     * Add command argument
335
     * @param string $raw
336
     * @param string $description
337
     * @param mixed $default
338
     * @param bool $required
339
     * @param bool $variadic
340
     * @param callable|null $filter
341
     * @return $this
342
     */
343
    public function addArgument(
344
        string $raw,
345
        string $description = '',
346
        mixed $default = null,
347
        bool $required = false,
348
        bool $variadic = false,
349
        ?callable $filter = null
350
    ): self {
351
        $argument = new Argument(
352
            $raw,
353
            $description,
354
            $default,
355
            $required,
356
            $variadic,
357
            $filter
358
        );
359
360
        if ($this->argVariadic) {
361
            throw new InvalidParameterException('Only last argument can be variadic');
362
        }
363
364
        if ($argument->isVariadic()) {
365
            $this->argVariadic = true;
366
        }
367
368
        $this->register($argument);
369
370
        return $this;
371
    }
372
373
    /**
374
     * Return all command options (i.e without defaults).
375
     * @return array<Option>
376
     */
377
    public function commandOptions(): array
378
    {
379
        $options = $this->options;
380
381
        unset($options['help']);
382
        unset($options['version']);
383
        unset($options['verbosity']);
384
385
        return $options;
386
    }
387
388
    /**
389
     * Show this command help and abort
390
     * @return mixed
391
     */
392
    public function showHelp(): mixed
393
    {
394
        $io = $this->io();
395
        $helper = new OutputHelper($io->writer());
396
397
        $io->writer()
398
            ->bold(
399
                sprintf(
400
                    'Command %s, version %s',
401
                    $this->name,
402
                    $this->version
403
                ),
404
                true
405
            )->eol();
406
407
        $io->writer()
408
                ->dim($this->description, true)->eol();
409
410
        $io->writer()
411
            ->bold(
412
                'Usage: '
413
            );
414
415
        $io->writer()
416
            ->yellow(
417
                sprintf('%s [OPTIONS...] [ARGUMENTS...]', $this->name),
418
                true
419
            );
420
421
        $helper->showArgumentsHelp($this->arguments())
422
                ->showOptionsHelp(
423
                    $this->options(),
424
                    '',
425
                    'Legend: <required> [optional] variadic...'
426
                );
427
428
        if ($this->usage) {
429
            $helper->showUsage($this->usage, $this->name);
430
        }
431
432
        return $this->emit('_exit', 0);
433
    }
434
435
    /**
436
     * Show this command version and abort
437
     * @return mixed
438
     */
439
    public function showVersion(): mixed
440
    {
441
        $this->writer()->bold(
442
            sprintf(
443
                '%s, %s',
444
                $this->name,
445
                $this->version
446
            ),
447
            true
448
        );
449
450
        return $this->emit('_exit', 0);
451
    }
452
453
    /**
454
     * Execute the command
455
     * @return mixed
456
     */
457
    public function execute(): mixed
458
    {
459
        return 0;
460
    }
461
462
    /**
463
     * Performs user interaction if required to set some missing values.
464
     * @param Reader $reader
465
     * @param Writer $writer
466
     * @return void
467
     */
468
    public function interact(Reader $reader, Writer $writer): void
0 ignored issues
show
The parameter $writer is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

468
    public function interact(Reader $reader, /** @scrutinizer ignore-unused */ Writer $writer): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
The parameter $reader is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

468
    public function interact(/** @scrutinizer ignore-unused */ Reader $reader, Writer $writer): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
469
    {
470
    }
471
472
    /**
473
     * Tap return given object or if that is null then app instance.
474
     * This aids for chaining.
475
     * @param mixed $object
476
     * @return mixed
477
     */
478
    public function tap(mixed $object = null): mixed
479
    {
480
        return $object ?? $this->app;
481
    }
482
483
    /**
484
     * {@inheritdoc}
485
     */
486
    public function emit(string $event, mixed $value = null): mixed
487
    {
488
        if (empty($this->events[$event])) {
489
            return null;
490
        }
491
492
        return ($this->events[$event])($value);
493
    }
494
495
    /**
496
     * Return the value of the given option
497
     * @param string $longName
498
     * @return mixed|null
499
     */
500
    public function getOptionValue(string $longName): mixed
501
    {
502
        $values = $this->values();
503
504
        return array_key_exists($longName, $values)
505
                    ? $values[$longName]
506
                    : null;
507
    }
508
509
    /**
510
     * Return the value of the given argument
511
     * @param string $name
512
     * @return mixed|null
513
     */
514
    public function getArgumentValue(string $name): mixed
515
    {
516
        $values = $this->values();
517
518
        return array_key_exists($name, $values)
519
                    ? $values[$name]
520
                    : null;
521
    }
522
523
    /**
524
     * Return the input/output instance
525
     * @return Interactor
526
     */
527
    protected function io(): Interactor
528
    {
529
        if ($this->app !== null) {
530
            return $this->app->io();
531
        }
532
533
        return new Interactor();
534
    }
535
536
    /**
537
     * Return the writer instance
538
     * @return Writer
539
     */
540
    protected function writer(): Writer
541
    {
542
        return $this->io()->writer();
543
    }
544
545
    /**
546
     * {@inheritdoc}
547
     */
548
    protected function handleUnknown(string $arg, mixed $value = null): mixed
549
    {
550
        if ($this->allowUnknown) {
551
            return $this->set(Helper::toCamelCase($arg), $value);
552
        }
553
554
        $values = array_filter($this->values(false));
555
556
        // Has some value, error!
557
        if (empty($values)) {
558
            throw new RuntimeException(sprintf(
559
                'Unknown option [%s]',
560
                $arg
561
            ));
562
        }
563
564
        // Has no value, show help!
565
        return $this->showHelp();
566
    }
567
568
    /**
569
     * Sets default options and exit handler.
570
     * @return $this
571
     */
572
    protected function defaults(): self
573
    {
574
        $this->addOption('-h|--help', 'Show help')
575
              ->on([$this, 'showHelp']);
576
577
        $this->addOption('-v|--version', 'Show version')
578
             ->on([$this, 'showVersion']);
579
580
        $this->addOption('-V|--verbosity', 'Verbosity level', 0)
581
             ->on(function () {
582
                $this->set('verbosity', ++$this->verbosity);
583
584
                return false;
585
             });
586
587
        $this->onExit(function (int $exitCode = 0) {
588
            $this->terminate($exitCode);
589
        });
590
591
        return $this;
592
    }
593
594
    /**
595
     * Terminate the program
596
     * @codeCoverageIgnore
597
     * @param int $exitCode
598
     * @return void
599
     */
600
    protected function terminate(int $exitCode): void
601
    {
602
        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...
603
    }
604
}
605