1
|
|
|
<?php namespace Tarsana\Command; |
2
|
|
|
|
3
|
|
|
use Tarsana\Command\Commands\HelpCommand; |
4
|
|
|
use Tarsana\Command\Commands\VersionCommand; |
5
|
|
|
use Tarsana\Command\Console\Console; |
6
|
|
|
use Tarsana\Command\Console\ExceptionPrinter; |
7
|
|
|
use Tarsana\Command\Interfaces\Console\ConsoleInterface; |
8
|
|
|
use Tarsana\Command\Interfaces\Template\TemplateLoaderInterface; |
9
|
|
|
use Tarsana\Command\SubCommand; |
10
|
|
|
use Tarsana\Command\Template\TemplateLoader; |
11
|
|
|
use Tarsana\IO\Filesystem; |
12
|
|
|
use Tarsana\IO\FilesystemInterface; |
13
|
|
|
use Tarsana\Syntax\Exceptions\ParseException; |
14
|
|
|
use Tarsana\Syntax\Factory as S; |
15
|
|
|
use Tarsana\Syntax\Text as T; |
16
|
|
|
|
17
|
|
|
class Command { |
18
|
|
|
|
19
|
|
|
protected $name; |
20
|
|
|
protected $version; |
21
|
|
|
protected $description; |
22
|
|
|
|
23
|
|
|
protected $syntax; |
24
|
|
|
protected $descriptions; |
25
|
|
|
|
26
|
|
|
protected $options; |
27
|
|
|
protected $args; |
28
|
|
|
|
29
|
|
|
protected $action; |
30
|
|
|
protected $commands; |
31
|
|
|
|
32
|
|
|
protected $console; |
33
|
|
|
protected $fs; |
34
|
|
|
protected $templatesLoader; |
35
|
|
|
|
36
|
|
|
public static function create(callable $action = null) { |
37
|
|
|
$command = new Command; |
38
|
|
|
if (null !== $action) |
39
|
|
|
$command->action($action); |
40
|
|
|
return $command; |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
public function __construct() |
44
|
|
|
{ |
45
|
|
|
$this->commands([]) |
46
|
|
|
->setupSubCommands() |
47
|
|
|
->name('Unknown') |
48
|
|
|
->version('1.0.0') |
49
|
|
|
->description('...') |
50
|
|
|
->descriptions([]) |
51
|
|
|
->options([]) |
52
|
|
|
->console(new Console) |
53
|
|
|
->fs(new Filesystem('.')) |
54
|
|
|
->init(); |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* name getter and setter. |
59
|
|
|
* |
60
|
|
|
* @param string |
61
|
|
|
* @return mixed |
62
|
|
|
*/ |
63
|
|
View Code Duplication |
public function name(string $value = null) |
|
|
|
|
64
|
|
|
{ |
65
|
|
|
if (null === $value) { |
66
|
|
|
return $this->name; |
67
|
|
|
} |
68
|
|
|
$this->name = $value; |
69
|
|
|
return $this; |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* version getter and setter. |
74
|
|
|
* |
75
|
|
|
* @param string |
76
|
|
|
* @return mixed |
77
|
|
|
*/ |
78
|
|
|
public function version(string $value = null) |
79
|
|
|
{ |
80
|
|
|
if (null === $value) { |
81
|
|
|
return $this->version; |
82
|
|
|
} |
83
|
|
|
$this->version = $value; |
84
|
|
|
return $this; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* description getter and setter. |
89
|
|
|
* |
90
|
|
|
* @param string |
91
|
|
|
* @return mixed |
92
|
|
|
*/ |
93
|
|
View Code Duplication |
public function description(string $value = null) |
|
|
|
|
94
|
|
|
{ |
95
|
|
|
if (null === $value) { |
96
|
|
|
return $this->description; |
97
|
|
|
} |
98
|
|
|
$this->description = $value; |
99
|
|
|
return $this; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* descriptions getter and setter. |
104
|
|
|
* |
105
|
|
|
* @param string |
106
|
|
|
* @return mixed |
107
|
|
|
*/ |
108
|
|
|
public function descriptions(array $value = null) |
109
|
|
|
{ |
110
|
|
|
if (null === $value) { |
111
|
|
|
return $this->descriptions; |
112
|
|
|
} |
113
|
|
|
$this->descriptions = $value; |
114
|
|
|
return $this; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* syntax getter and setter. |
119
|
|
|
* |
120
|
|
|
* @param string|null $syntax |
121
|
|
|
* @return Syntax|self |
122
|
|
|
*/ |
123
|
|
|
public function syntax(string $syntax = null) |
124
|
|
|
{ |
125
|
|
|
if (null === $syntax) |
126
|
|
|
return $this->syntax; |
127
|
|
|
|
128
|
|
|
$this->syntax = S::syntax()->parse("{{$syntax}| }"); |
129
|
|
|
return $this; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* options getter and setter. |
134
|
|
|
* |
135
|
|
|
* @param array |
136
|
|
|
* @return mixed |
137
|
|
|
*/ |
138
|
|
|
public function options(array $options = null) |
139
|
|
|
{ |
140
|
|
|
if (null === $options) { |
141
|
|
|
return $this->options; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
$this->options = []; |
145
|
|
|
foreach($options as $option) |
146
|
|
|
$this->options[$option] = false; |
147
|
|
|
|
148
|
|
|
return $this; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* option getter. |
153
|
|
|
* |
154
|
|
|
* @param string |
155
|
|
|
* @return mixed |
156
|
|
|
*/ |
157
|
|
|
public function option(string $name) |
158
|
|
|
{ |
159
|
|
|
if (!array_key_exists($name, $this->options)) |
160
|
|
|
throw new \InvalidArgumentException("Unknown option '{$name}'"); |
161
|
|
|
return $this->options[$name]; |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* args getter and setter. |
166
|
|
|
* |
167
|
|
|
* @param stdClass |
168
|
|
|
* @return mixed |
169
|
|
|
*/ |
170
|
|
|
public function args(\stdClass $value = null) |
171
|
|
|
{ |
172
|
|
|
if (null === $value) { |
173
|
|
|
return $this->args; |
174
|
|
|
} |
175
|
|
|
$this->args = $value; |
176
|
|
|
return $this; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* console getter and setter. |
181
|
|
|
* |
182
|
|
|
* @param ConsoleInterface |
183
|
|
|
* @return mixed |
184
|
|
|
*/ |
185
|
|
View Code Duplication |
public function console(ConsoleInterface $value = null) |
|
|
|
|
186
|
|
|
{ |
187
|
|
|
if (null === $value) { |
188
|
|
|
return $this->console; |
189
|
|
|
} |
190
|
|
|
$this->console = $value; |
191
|
|
|
foreach ($this->commands as $name => $command) { |
192
|
|
|
$command->console = $value; |
193
|
|
|
} |
194
|
|
|
return $this; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* fs getter and setter. |
199
|
|
|
* |
200
|
|
|
* @param Tarsana\IO\Filesystem|string |
201
|
|
|
* @return mixed |
202
|
|
|
*/ |
203
|
|
|
public function fs($value = null) |
204
|
|
|
{ |
205
|
|
|
if (null === $value) { |
206
|
|
|
return $this->fs; |
207
|
|
|
} |
208
|
|
|
if (is_string($value)) |
209
|
|
|
$value = new Filesystem($value); |
210
|
|
|
$this->fs = $value; |
211
|
|
|
foreach ($this->commands as $name => $command) { |
212
|
|
|
$command->fs = $value; |
213
|
|
|
} |
214
|
|
|
return $this; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
/** |
218
|
|
|
* templatesLoader getter and setter. |
219
|
|
|
* |
220
|
|
|
* @param Tarsana\Command\Interfaces\Template\TemplateLoaderInterface |
221
|
|
|
* @return mixed |
222
|
|
|
*/ |
223
|
|
View Code Duplication |
public function templatesLoader(TemplateLoaderInterface $value = null) |
|
|
|
|
224
|
|
|
{ |
225
|
|
|
if (null === $value) { |
226
|
|
|
return $this->templatesLoader; |
227
|
|
|
} |
228
|
|
|
$this->templatesLoader = $value; |
229
|
|
|
foreach ($this->commands as $name => $command) { |
230
|
|
|
$command->templatesLoader = $value; |
231
|
|
|
} |
232
|
|
|
return $this; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
View Code Duplication |
public function templatesPath(string $path, string $cachePath = null) { |
|
|
|
|
236
|
|
|
$this->templatesLoader = new TemplateLoader($path, $cachePath); |
237
|
|
|
foreach ($this->commands as $name => $command) { |
238
|
|
|
$command->templatesLoader = $this->templatesLoader(); |
239
|
|
|
} |
240
|
|
|
return $this; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
public function template(string $name) { |
244
|
|
|
if (null === $this->templatesLoader) |
245
|
|
|
throw new \Exception("Please initialize the templates loader before trying to load templates!"); |
246
|
|
|
return $this->templatesLoader->load($name); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* action getter and setter. |
251
|
|
|
* |
252
|
|
|
* @param callable |
253
|
|
|
* @return mixed |
254
|
|
|
*/ |
255
|
|
View Code Duplication |
public function action(callable $value = null) |
|
|
|
|
256
|
|
|
{ |
257
|
|
|
if (null === $value) { |
258
|
|
|
return $this->action; |
259
|
|
|
} |
260
|
|
|
$this->action = $value; |
261
|
|
|
return $this; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* commands getter and setter. |
266
|
|
|
* |
267
|
|
|
* @param array |
268
|
|
|
* @return mixed |
269
|
|
|
*/ |
270
|
|
View Code Duplication |
public function commands(array $value = null) |
|
|
|
|
271
|
|
|
{ |
272
|
|
|
if (null === $value) { |
273
|
|
|
return $this->commands; |
274
|
|
|
} |
275
|
|
|
$this->commands = []; |
276
|
|
|
foreach ($value as $name => $command) { |
277
|
|
|
$this->command($name, $command); |
278
|
|
|
} |
279
|
|
|
return $this; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
public function command(string $name, Command $command = null) |
283
|
|
|
{ |
284
|
|
|
if (null === $command) { |
285
|
|
|
if (!array_key_exists($name, $this->commands)) |
286
|
|
|
throw new \InvalidArgumentException("subcommand '{$name}' not found!"); |
287
|
|
|
return $this->commands[$name]; |
288
|
|
|
} |
289
|
|
|
$this->commands[$name] = $command; |
290
|
|
|
return $this; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
protected function setupSubCommands() |
294
|
|
|
{ |
295
|
|
|
return $this->command('--help', new HelpCommand($this)) |
296
|
|
|
->command('--version', new VersionCommand($this)); |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
public function describe(string $name, string $description = null) |
300
|
|
|
{ |
301
|
|
|
if (null === $description) |
302
|
|
|
return array_key_exists($name, $this->descriptions) |
303
|
|
|
? $this->descriptions[$name] : ''; |
304
|
|
|
if (substr($name, 0, 2) == '--' && array_key_exists($name, $this->options())) { |
305
|
|
|
$this->descriptions[$name] = $description; |
306
|
|
|
return $this; |
307
|
|
|
} |
308
|
|
|
try { |
309
|
|
|
$this->syntax->field($name); |
310
|
|
|
// throws exception if field is missing |
311
|
|
|
$this->descriptions[$name] = $description; |
312
|
|
|
return $this; |
313
|
|
|
} catch (\Exception $e) { |
314
|
|
|
throw new \InvalidArgumentException("Unknown field '{$name}'"); |
315
|
|
|
} |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
public function run(array $args = null, array $options = [], bool $rawArgs = true) |
|
|
|
|
319
|
|
|
{ |
320
|
|
|
try { |
321
|
|
|
$this->clear(); |
322
|
|
|
|
323
|
|
|
if ($rawArgs) { |
324
|
|
|
if (null === $args) { |
325
|
|
|
$args = $GLOBALS['argv']; |
326
|
|
|
array_shift($args); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
if (!empty($args) && array_key_exists($args[0], $this->commands)) { |
330
|
|
|
$name = $args[0]; |
331
|
|
|
array_shift($args); |
332
|
|
|
return $this->command($name)->run($args); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
$this->parseArguments($args); |
336
|
|
|
} else { |
337
|
|
|
$this->args = (object) $args; |
338
|
|
View Code Duplication |
foreach ($options as $name) { |
|
|
|
|
339
|
|
|
if (!array_key_exists($name, $this->options)) |
340
|
|
|
throw new \Exception("Unknown option '{$name}'"); |
341
|
|
|
$this->options[$name] = true; |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
if (null === $this->action) |
346
|
|
|
$this->execute(); |
347
|
|
|
else |
348
|
|
|
($this->action)($this); |
349
|
|
|
} catch (\Exception $e) { |
350
|
|
|
$this->handleError($e); |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
public function clear() |
355
|
|
|
{ |
356
|
|
|
$this->args = null; |
357
|
|
|
foreach($this->options as $name => $value) { |
358
|
|
|
$this->options[$name] = false; |
359
|
|
|
} |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
public function parseArguments(array $args) |
363
|
|
|
{ |
364
|
|
|
if (null === $this->syntax) { |
365
|
|
|
$this->args = null; |
366
|
|
|
return; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
$arguments = []; |
370
|
|
View Code Duplication |
foreach ($args as &$arg) { |
|
|
|
|
371
|
|
|
if (array_key_exists($arg, $this->options)) |
372
|
|
|
$this->options[$arg] = true; |
373
|
|
|
else |
374
|
|
|
$arguments[] = $arg; |
375
|
|
|
} |
376
|
|
|
$arguments = T::join($arguments, ' '); |
377
|
|
|
$this->args = $this->syntax->parse($arguments); |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
protected function handleError(\Exception $e) { |
381
|
|
|
$output = (new ExceptionPrinter)->print($e); |
382
|
|
|
$this->console()->error($output); |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
protected function init() {} |
386
|
|
|
protected function execute() {} |
387
|
|
|
|
388
|
|
|
} |
389
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.