Command   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 409
Duplicated Lines 0 %

Importance

Changes 10
Bugs 1 Features 2
Metric Value
eloc 92
c 10
b 1
f 2
dl 0
loc 409
rs 9.44
wmc 37

24 Methods

Rating   Name   Duplication   Size   Complexity  
A argument() 0 15 3
A desc() 0 3 1
A version() 0 5 1
A alias() 0 9 2
A onExit() 0 5 1
A on() 0 7 1
A app() 0 3 1
A defaults() 0 17 1
A handleUnknown() 0 17 3
A userOptions() 0 7 1
A __construct() 0 8 1
A usage() 0 9 2
A bind() 0 5 1
A option() 0 7 1
A name() 0 3 1
A arguments() 0 9 2
A emit() 0 7 2
A writer() 0 3 2
A io() 0 3 2
A interact() 0 2 1
A showVersion() 0 5 1
A tap() 0 3 1
A showHelp() 0 18 2
A action() 0 9 3
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\Input;
13
14
use Ahc\Cli\Application as App;
15
use Ahc\Cli\Exception\InvalidParameterException;
16
use Ahc\Cli\Exception\RuntimeException;
17
use Ahc\Cli\Helper\InflectsString;
18
use Ahc\Cli\Helper\OutputHelper;
19
use Ahc\Cli\IO\Interactor;
20
use Ahc\Cli\Output\Writer;
21
22
/**
23
 * Parser aware Command for the cli (based on tj/commander.js).
24
 *
25
 * @author  Jitendra Adhikari <[email protected]>
26
 * @license MIT
27
 *
28
 * @link    https://github.com/adhocore/cli
29
 */
30
class Command extends Parser
31
{
32
    use InflectsString;
33
34
    /** @var callable */
35
    protected $_action;
36
37
    /** @var string */
38
    protected $_version;
39
40
    /** @var string */
41
    protected $_name;
42
43
    /** @var string */
44
    protected $_desc;
45
46
    /** @var string Usage examples */
47
    protected $_usage;
48
49
    /** @var string Command alias */
50
    protected $_alias;
51
52
    /** @var App The cli app this command is bound to */
53
    protected $_app;
54
55
    /** @var callable[] Events for options */
56
    private $_events = [];
57
58
    /** @var bool Whether to allow unknown (not registered) options */
59
    private $_allowUnknown = false;
60
61
    /** @var bool If the last seen arg was variadic */
62
    private $_argVariadic = false;
63
64
    /**
65
     * Constructor.
66
     *
67
     * @param string $name
68
     * @param string $desc
69
     * @param bool   $allowUnknown
70
     * @param App    $app
71
     */
72
    public function __construct(string $name, string $desc = '', bool $allowUnknown = false, App $app = null)
73
    {
74
        $this->_name         = $name;
75
        $this->_desc         = $desc;
76
        $this->_allowUnknown = $allowUnknown;
77
        $this->_app          = $app;
78
79
        $this->defaults();
80
    }
81
82
    /**
83
     * Sets default options, actions and exit handler.
84
     *
85
     * @return self
86
     */
87
    protected function defaults(): self
88
    {
89
        $this->option('-h, --help', 'Show help')->on([$this, 'showHelp']);
90
        $this->option('-V, --version', 'Show version')->on([$this, 'showVersion']);
91
        $this->option('-v, --verbosity', 'Verbosity level', null, 0)->on(function () {
92
            $this->set('verbosity', ($this->verbosity ?? 0) + 1);
0 ignored issues
show
Bug Best Practice introduced by
The property verbosity does not exist on Ahc\Cli\Input\Command. Since you implemented __get, consider adding a @property annotation.
Loading history...
93
94
            return false;
95
        });
96
97
        // @codeCoverageIgnoreStart
98
        $this->onExit(function ($exitCode = 0) {
99
            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...
100
        });
101
        // @codeCoverageIgnoreEnd
102
103
        return $this;
104
    }
105
106
    /**
107
     * Sets version.
108
     *
109
     * @param string $version
110
     *
111
     * @return self
112
     */
113
    public function version(string $version): self
114
    {
115
        $this->_version = $version;
116
117
        return $this;
118
    }
119
120
    /**
121
     * Gets command name.
122
     *
123
     * @return string
124
     */
125
    public function name(): string
126
    {
127
        return $this->_name;
128
    }
129
130
    /**
131
     * Gets command description.
132
     *
133
     * @return string
134
     */
135
    public function desc(): string
136
    {
137
        return $this->_desc;
138
    }
139
140
    /**
141
     * Get the app this command belongs to.
142
     *
143
     * @return null|App
144
     */
145
    public function app()
146
    {
147
        return $this->_app;
148
    }
149
150
    /**
151
     * Bind command to the app.
152
     *
153
     * @param App|null $app
154
     *
155
     * @return self
156
     */
157
    public function bind(App $app = null): self
158
    {
159
        $this->_app = $app;
160
161
        return $this;
162
    }
163
164
    /**
165
     * Registers argument definitions (all at once). Only last one can be variadic.
166
     *
167
     * @param string $definitions
168
     *
169
     * @return self
170
     */
171
    public function arguments(string $definitions): self
172
    {
173
        $definitions = \explode(' ', $definitions);
174
175
        foreach ($definitions as $raw) {
176
            $this->argument($raw);
177
        }
178
179
        return $this;
180
    }
181
182
    /**
183
     * Register an argument.
184
     *
185
     * @param string $raw
186
     * @param string $desc
187
     * @param mixed  $default
188
     *
189
     * @return self
190
     */
191
    public function argument(string $raw, string $desc = '', $default = null): self
192
    {
193
        $argument = new Argument($raw, $desc, $default);
194
195
        if ($this->_argVariadic) {
196
            throw new InvalidParameterException('Only last argument can be variadic');
197
        }
198
199
        if ($argument->variadic()) {
200
            $this->_argVariadic = true;
201
        }
202
203
        $this->register($argument);
204
205
        return $this;
206
    }
207
208
    /**
209
     * Registers new option.
210
     *
211
     * @param string        $raw
212
     * @param string        $desc
213
     * @param callable|null $filter
214
     * @param mixed         $default
215
     *
216
     * @return self
217
     */
218
    public function option(string $raw, string $desc = '', callable $filter = null, $default = null): self
219
    {
220
        $option = new Option($raw, $desc, $default, $filter);
221
222
        $this->register($option);
223
224
        return $this;
225
    }
226
227
    /**
228
     * Gets user options (i.e without defaults).
229
     *
230
     * @return array
231
     */
232
    public function userOptions(): array
233
    {
234
        $options = $this->allOptions();
235
236
        unset($options['help'], $options['version'], $options['verbosity']);
237
238
        return $options;
239
    }
240
241
    /**
242
     * Gets or sets usage info.
243
     *
244
     * @param string|null $usage
245
     *
246
     * @return string|self
247
     */
248
    public function usage(string $usage = null)
249
    {
250
        if (\func_num_args() === 0) {
251
            return $this->_usage;
252
        }
253
254
        $this->_usage = $usage;
255
256
        return $this;
257
    }
258
259
    /**
260
     * Gets or sets alias.
261
     *
262
     * @param string|null $alias
263
     *
264
     * @return string|self
265
     */
266
    public function alias(string $alias = null)
267
    {
268
        if (\func_num_args() === 0) {
269
            return $this->_alias;
270
        }
271
272
        $this->_alias = $alias;
273
274
        return $this;
275
    }
276
277
    /**
278
     * Sets event handler for last (or given) option.
279
     *
280
     * @param callable $fn
281
     * @param string   $option
282
     *
283
     * @return self
284
     */
285
    public function on(callable $fn, string $option = null): self
286
    {
287
        $names = \array_keys($this->allOptions());
288
289
        $this->_events[$option ?? \end($names)] = $fn;
290
291
        return $this;
292
    }
293
294
    /**
295
     * Register exit handler.
296
     *
297
     * @param callable $fn
298
     *
299
     * @return self
300
     */
301
    public function onExit(callable $fn): self
302
    {
303
        $this->_events['_exit'] = $fn;
304
305
        return $this;
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311
    protected function handleUnknown(string $arg, string $value = null)
312
    {
313
        if ($this->_allowUnknown) {
314
            return $this->set($this->toCamelCase($arg), $value);
315
        }
316
317
        $values = \array_filter($this->values(false));
318
319
        // Has some value, error!
320
        if ($values) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $values of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
321
            throw new RuntimeException(
322
                \sprintf('Option "%s" not registered', $arg)
323
            );
324
        }
325
326
        // Has no value, show help!
327
        return $this->showHelp();
328
    }
329
330
    /**
331
     * Shows command help then aborts.
332
     *
333
     * @return mixed
334
     */
335
    public function showHelp()
336
    {
337
        $io     = $this->io();
338
        $helper = new OutputHelper($io->writer());
339
340
        $io->bold("Command {$this->_name}, version {$this->_version}", true)->eol();
341
        $io->comment($this->_desc, true)->eol();
342
        $io->bold('Usage: ')->yellow("{$this->_name} [OPTIONS...] [ARGUMENTS...]", true);
343
344
        $helper
345
            ->showArgumentsHelp($this->allArguments())
346
            ->showOptionsHelp($this->allOptions(), '', 'Legend: <required> [optional] variadic...');
347
348
        if ($this->_usage) {
349
            $helper->showUsage($this->_usage);
350
        }
351
352
        return $this->emit('_exit', 0);
353
    }
354
355
    /**
356
     * Shows command version then aborts.
357
     *
358
     * @return mixed
359
     */
360
    public function showVersion()
361
    {
362
        $this->writer()->bold($this->_version, true);
363
364
        return $this->emit('_exit', 0);
365
    }
366
367
    /**
368
     * {@inheritdoc}
369
     */
370
    public function emit(string $event, $value = null)
371
    {
372
        if (empty($this->_events[$event])) {
373
            return null;
374
        }
375
376
        return ($this->_events[$event])($value);
377
    }
378
379
    /**
380
     * Tap return given object or if that is null then app instance. This aids for chaining.
381
     *
382
     * @param mixed $object
383
     *
384
     * @return mixed
385
     */
386
    public function tap($object = null)
387
    {
388
        return $object ?? $this->_app;
389
    }
390
391
    /**
392
     * Performs user interaction if required to set some missing values.
393
     *
394
     * @param Interactor $io
395
     *
396
     * @return void
397
     */
398
    public function interact(Interactor $io)
0 ignored issues
show
Unused Code introduced by
The parameter $io 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

398
    public function interact(/** @scrutinizer ignore-unused */ Interactor $io)

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...
399
    {
400
        // Subclasses will do the needful.
401
    }
402
403
    /**
404
     * Get or set command action.
405
     *
406
     * @param callable|null $action If provided it is set
407
     *
408
     * @return callable|self If $action provided then self, otherwise the preset action.
409
     */
410
    public function action(callable $action = null)
411
    {
412
        if (\func_num_args() === 0) {
413
            return $this->_action;
414
        }
415
416
        $this->_action = $action instanceof \Closure ? \Closure::bind($action, $this) : $action;
417
418
        return $this;
419
    }
420
421
    /**
422
     * Get a writer instance.
423
     *
424
     * @return Writer
425
     */
426
    protected function writer(): Writer
427
    {
428
        return $this->_app ? $this->_app->io()->writer() : new Writer;
429
    }
430
431
    /**
432
     * Get IO instance.
433
     *
434
     * @return Interactor
435
     */
436
    protected function io(): Interactor
437
    {
438
        return $this->_app ? $this->_app->io() : new Interactor;
439
    }
440
}
441