Passed
Push — develop ( add880...26f5dd )
by nguereza
02:36
created

OutputHelper::sortItems()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 2
dl 0
loc 13
rs 10
c 0
b 0
f 0
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   http://www.iacademy.cf
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;
0 ignored issues
show
Bug introduced by
The type Platine\Console\Command\Command was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
52
use Platine\Console\Exception\ConsoleException;
53
use Platine\Console\Input\Option;
54
use Platine\Console\Input\Parameter;
55
use Platine\Console\Output\Color;
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
    /**
67
     * The writer stream instance
68
     * @var Writer
69
     */
70
    protected Writer $writer;
71
72
    /**
73
     * Max width of command name
74
     * @var int
75
     */
76
    protected int $maxCommandWidth = 0;
77
78
    /**
79
     * Create new instance
80
     * @param Writer|null $writer
81
     */
82
    public function __construct(?Writer $writer = null)
83
    {
84
        $this->writer = $writer ? $writer : new Writer();
85
    }
86
87
    /**
88
     * Print stack trace and error message of an exception.
89
     * @param Throwable $error
90
     * @return void
91
     */
92
    public function printTrace(Throwable $error): void
93
    {
94
        $errorClass = get_class($error);
95
96
        $this->writer->colors(sprintf(
97
            '%s <error>%s</end></eol> (thrown in <ok>%s</end>'
98
                . '<comment>:%s)</end></eol></eol>',
99
            $errorClass,
100
            $error->getMessage(),
101
            $error->getFile(),
102
            $error->getLine(),
103
        ));
104
105
        if ($error instanceof ConsoleException) {
106
            // Internal exception traces are not printed.
107
            return;
108
        }
109
110
        $traceStr = '</eol></eol><info>Stack Trace:</end></eol></eol>';
111
        foreach ($error->getTrace() as $i => $trace) {
112
            $trace += [
113
               'class' => '',
114
               'type' => '',
115
               'function' => '',
116
               'file' => '',
117
               'line' => '',
118
               'args' => []
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 array<\Platine\Console\Input\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 array<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 array<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()
205
                 ->green('Usage Examples: ', true, null, Color::BOLD)
206
                 ->colors($usage)->eol();
207
208
            return $this;
209
        }
210
211
        $lines = explode("\n", str_replace(
212
            ['</eol>', "\r\n"],
213
            "\n",
214
            $usage
215
        ));
216
217
        if (!is_array($lines)) {
0 ignored issues
show
introduced by
The condition is_array($lines) is always true.
Loading history...
218
            return $this;
219
        }
220
221
        foreach ($lines as $i => &$pos) {
222
            $replace = preg_replace('~</?\w+>~', '', $pos);
223
            if ($replace !== null) {
224
                $pos = strrpos($replace, ' ##');
225
            }
226
            if ($pos === false) {
227
                unset($lines[$i]);
228
            }
229
        }
230
231
        $maxLength = (int) max($lines) + 4;
232
        $formatedUsage = (string) preg_replace_callback(
233
            '~ ## ~',
234
            function () use (&$lines, $maxLength) {
235
                $sizeOfLine = 0;
236
                $currentLine = array_shift($lines);
237
                if ($currentLine !== null) {
238
                    $sizeOfLine = (int) $currentLine;
239
                }
240
                return str_pad('# ', $maxLength - $sizeOfLine, ' ', STR_PAD_LEFT);
241
            },
242
            $usage
243
        );
244
245
        $this->writer->eol()
246
                 ->green('Usage Examples: ', true, null, Color::BOLD)
247
                 ->colors($formatedUsage)->eol();
248
249
        return $this;
250
    }
251
252
    /**
253
     * Show command not found error
254
     * @param string $command
255
     * @param array<int, string> $available
256
     * @return $this
257
     */
258
    public function showCommandNotFound(string $command, array $available): self
259
    {
260
        $closest = [];
261
262
        foreach ($available as $cmd) {
263
            $lev = levenshtein($command, $cmd);
264
            if ($lev > 0 || $lev < 5) {
265
                $closest[$cmd] = $lev;
266
            }
267
        }
268
269
        $this->writer->writeError(sprintf(
270
            'Command "%s" not found',
271
            $command
272
        ), true);
273
274
        if (!empty($closest)) {
275
            asort($closest);
276
            $choosen = key($closest);
277
278
            $this->writer->eol()->bgRed(
279
                sprintf('Did you mean %s ?', $choosen),
280
                true
281
            );
282
        }
283
284
        return $this;
285
    }
286
287
    /**
288
     * Convert arguments to string
289
     * @param array<int, mixed> $args
290
     * @return string
291
     */
292
    protected function stringifyArgs(array $args): string
293
    {
294
        $holder = [];
295
        foreach ($args as $arg) {
296
            $holder[] = $this->stringifyArg($arg);
297
        }
298
299
        return implode(', ', $holder);
300
    }
301
302
    /**
303
     * Convert one argument to string
304
     * @param mixed $arg
305
     * @return string
306
     */
307
    protected function stringifyArg($arg): string
308
    {
309
        if (is_scalar($arg)) {
310
            return var_export($arg, true);
311
        }
312
313
        if (is_object($arg)) {
314
            return method_exists($arg, '__toString')
315
                    ? (string) $arg
316
                    : get_class($arg);
317
        }
318
319
        if (is_array($arg)) {
320
            return '[' . $this->stringifyArgs($arg) . ']';
321
        }
322
323
        return gettype($arg);
324
    }
325
326
    /**
327
     * Show help for given type (option, argument, command)
328
     * with header and footer
329
     * @param string $type
330
     * @param array<Parameter|Command> $items
331
     * @param string $header
332
     * @param string $footer
333
     * @return void
334
     */
335
    protected function showHelp(
336
        string $type,
337
        array $items,
338
        string $header = '',
339
        string $footer = ''
340
    ): void {
341
        if ($header) {
342
            $this->writer->bold($header, true);
343
        }
344
345
        $this->writer->eol()->green(
346
            $type . ':',
347
            true,
348
            null,
349
            Color::BOLD
350
        );
351
352
        if (empty($items)) {
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 ($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 array<Command|Parameter> $items
385
     * @param int $max
386
     * @return array<Command|Parameter>
387
     */
388
    protected function sortItems(array $items, int &$max = 0): array
389
    {
390
        $max = max(array_map(function ($item) {
391
            return strlen($this->getName($item));
392
        }, $items));
393
394
        uasort($items, function ($a, $b) {
395
            /* @var Parameter $b */
396
            /* @var Parameter $a */
397
            return $a->getName() <=> $b->getName();
398
        });
399
400
        return $items;
401
    }
402
403
    /**
404
     * Prepare name for different items.
405
     * @param Parameter|Command $item
406
     * @return string
407
     */
408
    protected function getName($item): string
409
    {
410
        $name = $item->getName();
411
        if ($item instanceof Command) {
412
            return trim(
413
                str_pad($name, $this->maxCommandWidth)
414
                    . ' ' . $item->getAlias()
0 ignored issues
show
Bug introduced by
The method getAlias() does not exist on Platine\Console\Input\Parameter. ( Ignorable by Annotation )

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

414
                    . ' ' . $item->/** @scrutinizer ignore-call */ getAlias()

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
415
            );
416
        }
417
418
        return $this->getLabel($item);
419
    }
420
421
    /**
422
     * Get parameter label for humans readable
423
     * @param Parameter $item
424
     * @return string
425
     */
426
    protected function getLabel(Parameter $item): string
427
    {
428
        $name = $item->getName();
429
        if ($item instanceof Option) {
430
            $name = $item->getShort() . '|' . $item->getLong();
431
        }
432
433
        $variadic = $item->isVariadic() ? '...' : '';
434
435
        if ($item->isRequired()) {
436
            return sprintf('<%s%s>', $name, $variadic);
437
        }
438
439
        return sprintf('[%s%s]', $name, $variadic);
440
    }
441
}
442