Passed
Push — master ( 42d472...9d6aee )
by Timm
02:01
created

Cmd::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 8
rs 10
1
<?php
2
3
namespace Stefaminator\Cli;
4
5
use Exception;
6
use GetOptionKit\OptionCollection;
7
use GetOptionKit\OptionResult;
8
use ReflectionFunction;
9
use RuntimeException;
10
11
12
class Cmd {
13
14
    /**
15
     * @var Cmd
16
     */
17
    public $parent;
18
19
    /**
20
     * @var string
21
     */
22
    public $cmd;
23
24
    /**
25
     * @var string
26
     */
27
    public $descr;
28
29
    /**
30
     * @var array
31
     */
32
    public $optionSpecs;
33
34
    /**
35
     * @var OptionCollection
36
     */
37
    private $optionCollection;
38
39
    /**
40
     * @var OptionResult|null
41
     */
42
    public $optionResult;
43
44
    /**
45
     * @var Exception
46
     */
47
    public $optionParseException;
48
49
    /**
50
     * @var string[]
51
     */
52
    public $arguments = [];
53
54
    /**
55
     * @var Cmd[]
56
     */
57
    private $subcommands = [];
58
59
    /**
60
     * @var callable|null
61
     */
62
    private $callable;
63
64
65
    public function __construct(string $cmd) {
66
        $this->cmd = $cmd;
67
        if ($cmd !== 'help') {
68
            $this->addSubCmd(
69
                self::extend('help')
70
                    ->setDescription('Displays help for this command.')
71
                    ->setCallable(static function(Cmd $cmd) {
72
                        $cmd->parent->help();
73
                    })
74
            );
75
        }
76
    }
77
78
    public function addOption(string $specString, array $config): self {
79
80
        $this->optionSpecs[$specString] = $config;
81
82
        return $this;
83
    }
84
85
    public function addSubCmd(Cmd $cmd): self {
86
87
        $cmd->parent = $this;
88
        $this->subcommands[$cmd->cmd] = $cmd;
89
90
        return $this;
91
    }
92
93
    public function setDescription(string $descr): self {
94
95
        $this->descr = $descr;
96
97
        return $this;
98
    }
99
100
    /**
101
     * @param callable $callable
102
     * @return $this
103
     */
104
    public function setCallable(callable $callable): self {
105
106
        try {
107
            $this->callable = $this->validateCallable($callable);
108
109
        } catch (Exception $e) {
110
            echo __METHOD__ . ' has been called with invalid callable: ' . $e->getMessage() . "\n";
111
        }
112
113
114
        return $this;
115
    }
116
117
    public function hasSubCmd(string $cmd): bool {
118
        return array_key_exists($cmd, $this->subcommands);
119
    }
120
121
    public function hasProvidedOption(string $key): bool {
122
        return $this->optionResult !== null && $this->optionResult->has($key);
123
    }
124
125
    public function getProvidedOption(string $key) {
126
        if($this->optionResult !== null) {
127
            return $this->optionResult->get($key);
128
        }
129
        return null;
130
    }
131
132
    public function getSubCmd(string $cmd): ?Cmd {
133
        if ($this->hasSubCmd($cmd)) {
134
            return $this->subcommands[$cmd];
135
        }
136
        return null;
137
    }
138
139
    public function getMethodName(): string {
140
        $cmd = $this;
141
        $pwd = [];
142
143
        while ($cmd !== null) {
144
            $pwd[] = $cmd->parent !== null ? $cmd->cmd : 'cmd';
145
            $cmd = $cmd->parent;
146
        }
147
148
        $pwd = array_reverse($pwd);
149
150
        $pwd_str = '';
151
        foreach ($pwd as $p) {
152
            $pwd_str .= ucfirst(strtolower($p));
153
        }
154
155
        return lcfirst($pwd_str);
156
    }
157
158
    public function getCallable(): ?callable {
159
        return $this->callable;
160
    }
161
162
    public function getOptionCollection(): OptionCollection {
163
164
        if($this->optionCollection !== null) {
165
            return $this->optionCollection;
166
        }
167
168
        $specs = (array)$this->optionSpecs;
169
170
        $collection = new OptionCollection();
171
172
        foreach ($specs as $k => $v) {
173
            $opt = $collection->add($k, $v['description']);
174
            if (array_key_exists('isa', $v)) {
175
                $opt->isa($v['isa']);
176
            }
177
            if (array_key_exists('default', $v)) {
178
                $opt->defaultValue($v['default']);
179
            }
180
        }
181
182
        $this->optionCollection = $collection;
183
        return $this->optionCollection;
184
    }
185
186
    public function handleOptionParseException(): bool {
187
188
        if($this->optionParseException === null) {
189
            return false;
190
        }
191
192
        App::eol();
193
        App::echo('Uups, something went wrong!', Color::FOREGROUND_COLOR_RED);
194
        App::eol();
195
        App::echo($this->optionParseException->getMessage(), Color::FOREGROUND_COLOR_RED);
196
        App::eol();
197
198
        $this->help();
199
200
        return true;
201
    }
202
203
    public function help(): void {
204
205
206
        $help = <<<EOT
207
208
              o       
209
           ` /_\ '    
210
          - (o o) -   
211
----------ooO--(_)--Ooo----------
212
          Need help?
213
---------------------------------  
214
EOT;
215
216
217
        App::echo($help, Color::FOREGROUND_COLOR_YELLOW);
218
219
        App::eol();
220
221
        $oc = $this->getOptionCollection();
222
        $has_options = !empty($oc->options);
223
224
        $has_subcommands = !empty($this->subcommands);
225
226
        App::eol();
227
        App::echo('Usage: ', Color::FOREGROUND_COLOR_YELLOW);
228
        App::eol();
229
230
        App::echo(
231
            '  ' .
232
            ($this->parent !== null ? $this->cmd : 'command') .
233
            ($has_options ? ' [options]' : '') .
234
            ($has_subcommands ? ' [command]' : '')
235
        );
236
237
        App::eol();
238
239
240
241
        if ($has_options) {
242
243
            App::eol();
244
            App::echo('Options: ', Color::FOREGROUND_COLOR_YELLOW);
245
            App::eol();
246
247
            foreach ($oc->options as $option) {
248
249
                $s = '    ';
250
                if(!empty($option->short)) {
251
                    $s = '-' . $option->short . ', ';
252
                }
253
                $s .= '--' . $option->long;
254
255
                $s = '  ' . str_pad($s, 20, ' ');
256
                App::echo($s, Color::FOREGROUND_COLOR_GREEN);
257
258
                $s = ' ' . $option->desc;
259
                App::echo($s);
260
261
                if ($option->defaultValue) {
262
                    $s = ' [default: ' . $option->defaultValue . ']';
263
                    App::echo($s, Color::FOREGROUND_COLOR_YELLOW);
264
                }
265
266
                App::eol();
267
268
            }
269
270
            App::eol();
271
        }
272
273
        if($has_subcommands) {
274
275
            App::eol();
276
            App::echo('Available commands: ', Color::FOREGROUND_COLOR_YELLOW);
277
            App::eol();
278
279
            foreach ($this->subcommands as $cmd) {
280
281
                $s = '  ' . str_pad($cmd->cmd, 20, ' ');
282
                App::echo($s, Color::FOREGROUND_COLOR_GREEN);
283
284
                $s = ' ' . $cmd->descr;
285
                App::echo($s);
286
287
                App::eol();
288
            }
289
290
            App::eol();
291
        }
292
    }
293
294
    public static function extend(string $cmd): Cmd {
295
        return new class($cmd) extends Cmd {};
296
    }
297
298
    public static function root(): Cmd {
299
        return self::extend('__root');
300
    }
301
302
303
    /**
304
     * @param callable $callable
305
     * @return callable
306
     * @throws Exception
307
     */
308
    private function validateCallable(callable $callable): callable {
309
310
        $check = new ReflectionFunction($callable);
311
        $parameters = $check->getParameters();
312
313
        if (count($parameters) !== 1) {
314
            throw new RuntimeException('Invalid number of Parameters. Should be 1.');
315
        }
316
317
        $type = $parameters[0]->getType();
318
319
        if ($type === null) {
320
            throw new RuntimeException('Named type of Parameter 1 should be "' . __CLASS__ . '".');
321
        }
322
323
        /** @noinspection PhpPossiblePolymorphicInvocationInspection */
324
        $tname = $type->getName();
325
326
        if ($tname !== __CLASS__) {
327
            throw new RuntimeException('Named type of Parameter 1 should be "' . __CLASS__ . '".');
328
        }
329
330
        return $callable;
331
    }
332
}