OutputHelper::showUsage()   B
last analyzed

Complexity

Conditions 8
Paths 8

Size

Total Lines 53
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 32
nc 8
nop 2
dl 0
loc 53
rs 8.1635
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 OutputHelper.php
35
 *
36
 *  The console output helper class
37
 *
38
 *  @package    Platine\Console\Util
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\Util;
50
51
use Platine\Console\Command\Command;
52
use Platine\Console\Exception\ConsoleException;
53
use Platine\Console\Input\Argument;
54
use Platine\Console\Input\Option;
55
use Platine\Console\Input\Parameter;
56
use Platine\Console\Output\Writer;
57
use Throwable;
58
59
/**
60
 * @class OutputHelper
61
 * @package Platine\Console\Util
62
 */
63
class OutputHelper
64
{
65
    /**
66
     * The writer stream instance
67
     * @var Writer
68
     */
69
    protected Writer $writer;
70
71
    /**
72
     * Max width of command name
73
     * @var int
74
     */
75
    protected int $maxCommandWidth = 0;
76
77
    /**
78
     * Create new instance
79
     * @param Writer|null $writer
80
     */
81
    public function __construct(?Writer $writer = null)
82
    {
83
        $this->writer = $writer ?? new Writer();
84
    }
85
86
    /**
87
     * Print stack trace and error message of an exception.
88
     * @param Throwable $error
89
     * @return void
90
     */
91
    public function printTrace(Throwable $error): void
92
    {
93
        $errorClass = get_class($error);
94
95
        $this->writer->colors(sprintf(
96
            '%s <error>%s</end></eol> (thrown in <ok>%s</end>'
97
                . '<comment>:%s)</end></eol></eol>',
98
            $errorClass,
99
            $error->getMessage(),
100
            $error->getFile(),
101
            $error->getLine(),
102
        ));
103
104
        if ($error instanceof ConsoleException) {
105
            // Internal exception traces are not printed.
106
            return;
107
        }
108
109
        $traceStr = '</eol></eol><info>Stack Trace:</end></eol></eol>';
110
        foreach ($error->getTrace() as $i => $trace) {
111
            $trace += [
112
               'class' => '',
113
               'type' => '',
114
               'function' => '',
115
               'file' => '',
116
               'line' => '',
117
               'args' => []
118
               ];
119
120
            $symbol = $trace['class'] . $trace['type'] . $trace['function'];
121
            $arguments = $this->stringifyArgs($trace['args']);
122
123
            $traceStr .= ' <comment>' . $i . ')</end> <error>'
124
                   . $symbol . '</end><comment>(' . $arguments . ')</end>';
125
126
            if ($trace['file'] !== '') {
127
                $file = realpath($trace['file']);
128
                $traceStr .= '</eol>   <yellow>at ' . $file
129
                        . '</end><white>:' . $trace['line'] . '</end></eol>';
130
            }
131
        }
132
133
        $this->writer->colors($traceStr);
134
    }
135
136
    /**
137
     * Show arguments help
138
     * @param Argument[] $items
139
     * @param string $header
140
     * @param string $footer
141
     * @return $this
142
     */
143
    public function showArgumentsHelp(
144
        array $items,
145
        string $header = '',
146
        string $footer = ''
147
    ): self {
148
        $this->showHelp('Arguments', $items, $header, $footer);
149
150
        return $this;
151
    }
152
153
     /**
154
     * Show options help
155
     * @param Option[] $items
156
     * @param string $header
157
     * @param string $footer
158
     * @return $this
159
     */
160
    public function showOptionsHelp(
161
        array $items,
162
        string $header = '',
163
        string $footer = ''
164
    ): self {
165
        $this->showHelp('Options', $items, $header, $footer);
166
167
        return $this;
168
    }
169
170
    /**
171
     * Show commands help
172
     * @param Command[] $items
173
     * @param string $header
174
     * @param string $footer
175
     * @return $this
176
     */
177
    public function showCommandsHelp(
178
        array $items,
179
        string $header = '',
180
        string $footer = ''
181
    ): self {
182
        $this->maxCommandWidth = !empty($items) ? max(array_map(function (Command $item) {
183
            return strlen($item->getName());
184
        }, $items)) : 0;
185
186
        $this->showHelp('Commands', $items, $header, $footer);
187
188
        return $this;
189
    }
190
191
    /**
192
     * Show usage examples of a Command.
193
     * It replaces $0 with actual command name
194
     * and properly pads ` ## ` segments.
195
     * @param string $description
196
     * @param string $cmdName
197
     * @return $this
198
     */
199
    public function showUsage(string $description, string $cmdName = ''): self
200
    {
201
        $usage = str_replace('$0', $cmdName ? $cmdName : '[cmd]', $description);
202
203
        if (strpos($usage, ' ## ') === false) {
204
            $this->writer->eol()->boldGreen('Usage Examples: ', true)
205
                 ->colors($usage)->eol();
206
207
            return $this;
208
        }
209
210
        $lines = explode("\n", str_replace(
211
            ['</eol>', "\r\n"],
212
            "\n",
213
            $usage
214
        ));
215
216
        if (!is_array($lines)) {
217
            return $this;
218
        }
219
220
        foreach ($lines as $i => &$pos) {
221
            $replace = (string) preg_replace('~</?\w+>~', '', $pos);
222
            $pos = strrpos($replace, ' ##');
223
224
            if ($pos === false) {
225
                unset($lines[$i]);
226
            }
227
        }
228
229
        $maxLength = 0;
230
        if (count($lines) > 0) {
231
            $maxLength = (int) max($lines) + 4;
232
        }
233
234
        $formatedUsage = (string) preg_replace_callback(
235
            '~ ## ~',
236
            function () use (&$lines, $maxLength) {
237
                $sizeOfLine = 0;
238
                $currentLine = array_shift($lines);
239
                if ($currentLine !== null) {
240
                    $sizeOfLine = (int) $currentLine;
241
                }
242
                return str_pad('# ', $maxLength - $sizeOfLine, ' ', STR_PAD_LEFT);
243
            },
244
            $usage
245
        );
246
247
        $this->writer->eol()
248
                 ->boldGreen('Usage Examples: ', true)
249
                 ->colors($formatedUsage)->eol();
250
251
        return $this;
252
    }
253
254
    /**
255
     * Show command not found error
256
     * @param string $command
257
     * @param array<int, string> $available
258
     * @return $this
259
     */
260
    public function showCommandNotFound(string $command, array $available): self
261
    {
262
        $closest = [];
263
264
        foreach ($available as $cmd) {
265
            $lev = levenshtein($command, $cmd);
266
            if ($lev > 0 && $lev < 5) {
267
                $closest[$cmd] = $lev;
268
            }
269
        }
270
271
        $this->writer->writeError(sprintf(
272
            'Command "%s" not found',
273
            $command
274
        ), true);
275
276
        if (!empty($closest)) {
277
            asort($closest);
278
            $choosen = key($closest);
279
280
            $this->writer->eol()->bgRed(
281
                sprintf('Did you mean %s ?', $choosen),
282
                true
283
            );
284
        }
285
286
        return $this;
287
    }
288
289
    /**
290
     * Convert arguments to string
291
     * @param array<int, mixed> $args
292
     * @return string
293
     */
294
    protected function stringifyArgs(array $args): string
295
    {
296
        $holder = [];
297
        foreach ($args as $arg) {
298
            $holder[] = $this->stringifyArg($arg);
299
        }
300
301
        return implode(', ', $holder);
302
    }
303
304
    /**
305
     * Convert one argument to string
306
     * @param mixed $arg
307
     * @return string
308
     */
309
    protected function stringifyArg(mixed $arg): string
310
    {
311
        if (is_scalar($arg)) {
312
            return var_export($arg, true);
313
        }
314
315
        if (is_object($arg)) {
316
            return method_exists($arg, '__toString')
317
                    ? (string) $arg
318
                    : get_class($arg);
319
        }
320
321
        if (is_array($arg)) {
322
            return '[' . $this->stringifyArgs($arg) . ']';
323
        }
324
325
        return gettype($arg);
326
    }
327
328
    /**
329
     * Show help for given type (option, argument, command)
330
     * with header and footer
331
     * @param string $type
332
     * @param Parameter[]|Command[] $items
333
     * @param string $header
334
     * @param string $footer
335
     * @return void
336
     */
337
    protected function showHelp(
338
        string $type,
339
        array $items,
340
        string $header = '',
341
        string $footer = ''
342
    ): void {
343
        if (!empty($header)) {
344
            $this->writer->bold($header, true);
345
        }
346
347
        $this->writer->eol()->boldGreen(
348
            $type . ':',
349
            true
350
        );
351
352
        if (count($items) === 0) {
353
            $this->writer->bold('  (n/a)', true);
354
355
            return;
356
        }
357
358
        $space = 4;
359
        $padLength = 0;
360
        foreach ($this->sortItems($items, $padLength) as $item) {
361
            $name = $this->getName($item);
362
            $desc = str_replace(
363
                ["\r\n", "\n"],
364
                str_pad("\n", $padLength + $space + 3),
365
                $item->getDescription()
366
            );
367
368
            $this->writer->bold(
369
                str_pad(
370
                    $name,
371
                    $padLength + $space
372
                ),
373
            );
374
            $this->writer->dim($desc, true);
375
        }
376
377
        if (!empty($footer)) {
378
            $this->writer->eol()->yellow($footer, true);
379
        }
380
    }
381
382
    /**
383
     * Sort items by name. As a side effect sets max length of all names.
384
     * @param Command[]|Parameter[] $items
385
     * @param int $max
386
     * @return Command[]|Parameter[]
387
     */
388
    protected function sortItems(array $items, int &$max = 0): array
389
    {
390
        $results = array_map(function ($item) {
391
            return strlen($this->getName($item));
392
        }, $items);
393
394
        $max = 0;
395
        if (count($results) > 0) {
396
            $max = max($results);
397
        }
398
399
        uasort($items, function (Parameter|Command $a, Parameter|Command $b) {
400
            return $a->getName() <=> $b->getName();
401
        });
402
403
        return $items;
404
    }
405
406
    /**
407
     * Prepare name for different items.
408
     * @param Parameter|Command $item
409
     * @return string
410
     */
411
    protected function getName(Parameter|Command $item): string
412
    {
413
        $name = $item->getName();
414
        if ($item instanceof Command) {
415
            return trim(
416
                str_pad($name, $this->maxCommandWidth)
417
                    . ' ' . $item->getAlias()
418
            );
419
        }
420
421
        return $this->getLabel($item);
422
    }
423
424
    /**
425
     * Get parameter label for humans readable
426
     * @param Parameter $item
427
     * @return string
428
     */
429
    protected function getLabel(Parameter $item): string
430
    {
431
        $name = $item->getName();
432
        if ($item instanceof Option) {
433
            $name = $item->getShort() . '|' . $item->getLong();
434
        }
435
436
        $variadic = $item->isVariadic() ? '...' : '';
437
438
        if ($item->isRequired()) {
439
            return sprintf('<%s%s>', $name, $variadic);
440
        }
441
442
        return sprintf('[%s%s]', $name, $variadic);
443
    }
444
}
445