• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JBZoo / Cli / 7685763438

28 Jan 2024 12:35PM UTC coverage: 80.746% (-3.1%) from 83.803%
7685763438

push

github

web-flow
Add support for PHP 8.3 and update Symfony dependencies to ^6.4 (#24)

* Add support for PHP 8.3 and update Symfony dependencies to ^6.4
* Fix default value handling in CliCommand

0 of 3 new or added lines in 1 file covered. (0.0%)

41 existing lines in 5 files now uncovered.

952 of 1179 relevant lines covered (80.75%)

234.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.95
/src/CliCommand.php
1
<?php
2

3
/**
4
 * JBZoo Toolbox - Cli.
5
 *
6
 * This file is part of the JBZoo Toolbox project.
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @license    MIT
11
 * @copyright  Copyright (C) JBZoo.com, All rights reserved.
12
 * @see        https://github.com/JBZoo/Cli
13
 */
14

15
declare(strict_types=1);
16

17
namespace JBZoo\Cli;
18

19
use JBZoo\Cli\OutputMods\AbstractOutputMode;
20
use JBZoo\Cli\OutputMods\Cron;
21
use JBZoo\Cli\OutputMods\Logstash;
22
use JBZoo\Cli\OutputMods\Text;
23
use JBZoo\Cli\ProgressBars\AbstractProgressBar;
24
use JBZoo\Utils\Arr;
25
use JBZoo\Utils\Str;
26
use JBZoo\Utils\Vars;
27
use Symfony\Component\Console\Command\Command;
28
use Symfony\Component\Console\Helper\QuestionHelper;
29
use Symfony\Component\Console\Input\InputInterface;
30
use Symfony\Component\Console\Input\InputOption;
31
use Symfony\Component\Console\Output\OutputInterface;
32
use Symfony\Component\Console\Question\ChoiceQuestion;
33
use Symfony\Component\Console\Question\ConfirmationQuestion;
34
use Symfony\Component\Console\Question\Question;
35

36
use function JBZoo\Utils\bool;
37
use function JBZoo\Utils\float;
38
use function JBZoo\Utils\int;
39

40
abstract class CliCommand extends Command
41
{
42
    protected AbstractOutputMode $outputMode;
43

44
    abstract protected function executeAction(): int;
45

46
    public function progressBar(
47
        int|iterable $listOrMax,
48
        \Closure $callback,
49
        string $title = '',
50
        bool $throwBatchException = true,
51
        ?AbstractOutputMode $outputMode = null,
52
    ): AbstractProgressBar {
53
        static $nestedLevel = 0;
252✔
54

55
        $outputMode ??= $this->outputMode;
252✔
56

57
        $progressBar = $outputMode->createProgressBar()
252✔
58
            ->setTitle($title)
252✔
59
            ->setCallback($callback)
252✔
60
            ->setThrowBatchException($throwBatchException);
252✔
61

62
        if (\is_iterable($listOrMax)) {
252✔
63
            $progressBar->setList($listOrMax);
90✔
64
        } else {
65
            $progressBar->setMax($listOrMax);
168✔
66
        }
67

68
        $nestedLevel++;
252✔
69
        $progressBar->setNestedLevel($nestedLevel);
252✔
70

71
        $progressBar->execute();
252✔
72

73
        $nestedLevel--;
198✔
74
        $progressBar->setNestedLevel($nestedLevel);
198✔
75

76
        return $progressBar;
198✔
77
    }
78

79
    /**
80
     * {@inheritDoc}
81
     */
82
    protected function configure(): void
83
    {
84
        $this
876✔
85
            ->addOption(
876✔
86
                'no-progress',
876✔
87
                null,
876✔
88
                InputOption::VALUE_NONE,
876✔
89
                'Disable progress bar animation for logs. ' .
876✔
90
                'It will be used only for <info>' . Text::getName() . '</info> output format.',
876✔
91
            )
876✔
92
            ->addOption(
876✔
93
                'mute-errors',
876✔
94
                null,
876✔
95
                InputOption::VALUE_NONE,
876✔
96
                "Mute any sort of errors. So exit code will be always \"0\" (if it's possible).\n" .
876✔
97
                "It has major priority then <info>--non-zero-on-error</info>. It's on your own risk!",
876✔
98
            )
876✔
99
            ->addOption(
876✔
100
                'stdout-only',
876✔
101
                null,
876✔
102
                InputOption::VALUE_NONE,
876✔
103
                "For any errors messages application will use StdOut instead of StdErr. It's on your own risk!",
876✔
104
            )
876✔
105
            ->addOption(
876✔
106
                'non-zero-on-error',
876✔
107
                null,
876✔
108
                InputOption::VALUE_NONE,
876✔
109
                'None-zero exit code on any StdErr message.',
876✔
110
            )
876✔
111
            ->addOption(
876✔
112
                'timestamp',
876✔
113
                null,
876✔
114
                InputOption::VALUE_NONE,
876✔
115
                'Show timestamp at the beginning of each message.' .
876✔
116
                'It will be used only for <info>' . Text::getName() . '</info> output format.',
876✔
117
            )
876✔
118
            ->addOption(
876✔
119
                'profile',
876✔
120
                null,
876✔
121
                InputOption::VALUE_NONE,
876✔
122
                'Display timing and memory usage information.',
876✔
123
            )
876✔
124
            ->addOption(
876✔
125
                'output-mode',
876✔
126
                null,
876✔
127
                InputOption::VALUE_REQUIRED,
876✔
128
                "Output format. Available options:\n" . CliHelper::renderListForHelpDescription([
876✔
129
                    Text::getName()     => Text::getDescription(),
876✔
130
                    Cron::getName()     => Cron::getDescription(),
876✔
131
                    Logstash::getName() => Logstash::getDescription(),
876✔
132
                ]),
876✔
133
                Text::getName(),
876✔
134
            )
876✔
135
            ->addOption(
876✔
136
                Cron::getName(),
876✔
137
                null,
876✔
138
                InputOption::VALUE_NONE,
876✔
139
                'Alias for <info>--output-mode=' . Cron::getName() . '</info>. <comment>Deprecated!</comment>',
876✔
140
            );
876✔
141

142
        parent::configure();
876✔
143
    }
144

145
    /**
146
     * {@inheritDoc}
147
     */
148
    protected function execute(InputInterface $input, OutputInterface $output): int
149
    {
150
        $this->outputMode = $this->createOutputMode($input, $output, self::getOutputFormat($input));
876✔
151
        $this->getCliApplication()->setOutputMode($this->outputMode);
876✔
152

153
        $exitCode = Codes::OK;
876✔
154

155
        try {
156
            $this->outputMode->onExecBefore();
876✔
157
            $this->trigger('exec.before', [$this, $this->outputMode]);
876✔
158
            \ob_start();
876✔
159
            $exitCode    = $this->executeAction();
876✔
160
            $echoContent = \ob_get_clean();
720✔
161
            if ($echoContent !== false && $echoContent !== '') {
720✔
162
                $this->showLegacyOutput($echoContent);
720✔
163
            }
164
        } catch (\Exception $exception) {
156✔
165
            $echoContent = \ob_get_clean();
156✔
166
            if ($echoContent !== false && $echoContent !== '') {
156✔
167
                $this->showLegacyOutput($echoContent);
90✔
168
            }
169

170
            $this->outputMode->onExecException($exception);
156✔
171
            $this->trigger('exception', [$this, $this->outputMode, $exception]);
156✔
172

173
            $this->outputMode->onExecAfter($exitCode);
156✔
174

175
            if (!$this->getOptBool('mute-errors')) {
156✔
176
                throw $exception;
132✔
177
            }
178
        }
179

180
        $exitCode = Vars::range($exitCode, 0, 255);
744✔
181

182
        if ($this->outputMode->isOutputHasErrors() && $this->getOptBool('non-zero-on-error')) {
744✔
183
            $exitCode = Codes::GENERAL_ERROR;
24✔
184
        }
185

186
        $this->outputMode->onExecAfter($exitCode);
744✔
187
        $this->trigger('exec.after', [$this, $this->outputMode, &$exitCode]);
744✔
188

189
        if ($this->getOptBool('mute-errors')) {
744✔
190
            $exitCode = 0;
24✔
191
        }
192

193
        return $exitCode;
744✔
194
    }
195

196
    /**
197
     * @return null|mixed
198
     */
199
    protected function getOpt(string $optionName, bool $canBeArray = true): mixed
200
    {
201
        $value = $this->outputMode->getInput()->getOption($optionName);
876✔
202

203
        if ($canBeArray && \is_array($value)) {
876✔
204
            return Arr::last($value);
174✔
205
        }
206

207
        return $value;
876✔
208
    }
209

210
    /**
211
     * @SuppressWarnings(PHPMD.BooleanGetMethodName)
212
     */
213
    protected function getOptBool(string $optionName): bool
214
    {
215
        $value = $this->getOpt($optionName);
876✔
216

217
        return bool($value);
876✔
218
    }
219

220
    /**
221
     * @param int[] $onlyExpectedOptions
222
     */
223
    protected function getOptInt(string $optionName, array $onlyExpectedOptions = []): int
224
    {
225
        $value  = $this->getOpt($optionName) ?? 0;
360✔
226
        $result = int($value);
360✔
227

228
        if (\count($onlyExpectedOptions) > 0 && !\in_array($result, $onlyExpectedOptions, true)) {
360✔
229
            throw new Exception(
×
230
                "Passed invalid value of option \"--{$optionName}={$result}\".\n" .
×
231
                'Strict expected int-values are only: ' . CliHelper::renderExpectedValues($onlyExpectedOptions),
×
UNCOV
232
            );
×
233
        }
234

235
        return int($value);
360✔
236
    }
237

238
    /**
239
     * @param float[] $onlyExpectedOptions
240
     */
241
    protected function getOptFloat(string $optionName, array $onlyExpectedOptions = []): float
242
    {
243
        $value  = $this->getOpt($optionName) ?? 0.0;
174✔
244
        $result = float($value);
174✔
245

246
        if (\count($onlyExpectedOptions) > 0 && !\in_array($result, $onlyExpectedOptions, true)) {
174✔
247
            throw new Exception(
×
248
                "Passed invalid value of option \"--{$optionName}={$result}\".\n" .
×
249
                'Strict expected float-values are only: ' . CliHelper::renderExpectedValues($onlyExpectedOptions),
×
UNCOV
250
            );
×
251
        }
252

253
        return $result;
174✔
254
    }
255

256
    /**
257
     * @param string[] $onlyExpectedOptions
258
     */
259
    protected function getOptString(string $optionName, string $default = '', array $onlyExpectedOptions = []): string
260
    {
261
        $value  = \trim((string)$this->getOpt($optionName));
822✔
262
        $length = \strlen($value);
822✔
263
        $result = $length > 0 ? $value : $default;
822✔
264

265
        if (\count($onlyExpectedOptions) > 0 && !\in_array($result, $onlyExpectedOptions, true)) {
822✔
266
            throw new Exception(
×
267
                "Passed invalid value of option \"--{$optionName}={$result}\".\n" .
×
268
                'Strict expected string-values are only: ' . CliHelper::renderExpectedValues($onlyExpectedOptions),
×
UNCOV
269
            );
×
270
        }
271

272
        return $result;
822✔
273
    }
274

275
    protected function getOptArray(string $optionName): array
276
    {
277
        $list = $this->getOpt($optionName, false) ?? [];
174✔
278

279
        return (array)$list;
174✔
280
    }
281

282
    /**
283
     * @param string[] $onlyExpectedOptions
284
     */
285
    protected function getOptDatetime(
286
        string $optionName,
287
        string $defaultDatetime = '1970-01-01 00:00:00',
288
        array $onlyExpectedOptions = [],
289
    ): \DateTimeImmutable {
290
        $value  = $this->getOptString($optionName);
×
291
        $result = $value === '' ? $defaultDatetime : $value;
×
292

293
        if (\count($onlyExpectedOptions) > 0 && !\in_array($result, $onlyExpectedOptions, true)) {
×
294
            throw new Exception(
×
295
                "Passed invalid value of option {$optionName}={$result}. " .
×
296
                'Strict expected string-values are only: ' . CliHelper::renderExpectedValues($onlyExpectedOptions),
×
UNCOV
297
            );
×
298
        }
299

300
        return new \DateTimeImmutable($result);
×
301
    }
302

303
    /**
304
     * Alias to write new line in std output.
305
     * @SuppressWarnings(PHPMD.CamelCaseMethodName)
306
     */
307
    protected function _(
308
        null|bool|float|int|iterable|string $messages = '',
309
        string $verboseLevel = '',
310
        array $context = [],
311
    ): void {
312
        $this->outputMode->_($messages, $verboseLevel, $context);
630✔
313
    }
314

315
    protected function isInfoLevel(): bool
316
    {
317
        return $this->outputMode->isInfoLevel();
×
318
    }
319

320
    protected function isWarningLevel(): bool
321
    {
322
        return $this->outputMode->isWarningLevel();
×
323
    }
324

325
    protected function isDebugLevel(): bool
326
    {
327
        return $this->outputMode->isDebugLevel();
×
328
    }
329

330
    protected function isProfile(): bool
331
    {
332
        return $this->outputMode->isDisplayProfiling();
×
333
    }
334

335
    protected function trigger(string $eventName, array $arguments = [], ?callable $continueCallback = null): int
336
    {
337
        $application = $this->getApplication();
876✔
338
        if ($application === null) {
876✔
339
            return 0;
×
340
        }
341

342
        if ($application instanceof CliApplication) {
876✔
343
            $eManager = $application->getEventManager();
876✔
344
            if ($eManager === null) {
876✔
345
                return 0;
876✔
346
            }
347

348
            return $eManager->trigger("jbzoo.cli.{$eventName}", $arguments, $continueCallback);
×
349
        }
350

351
        return 0;
×
352
    }
353

354
    protected function ask(string $question, string $default = '', bool $isHidden = false): string
355
    {
356
        $question     = \rtrim($question, ':');
×
357
        $questionText = "<yellow-r>Question:</yellow-r> {$question}";
×
358
        if (!$isHidden) {
×
359
            $questionText .= ($default !== '' ? " (Default: <i>{$default}</i>)" : '');
×
360
        }
361

362
        $questionObj = new Question($questionText . ': ', $default);
×
363
        if ($isHidden) {
×
364
            $questionObj->setHidden(true);
×
365
            $questionObj->setHiddenFallback(false);
×
366
        }
367

368
        return (string)$this->getQuestionHelper()->ask(
×
369
            $this->outputMode->getInput(),
×
370
            $this->outputMode->getOutput(),
×
UNCOV
371
            $questionObj,
×
UNCOV
372
        );
×
373
    }
374

375
    protected function askPassword(string $question, bool $randomDefault = true): string
376
    {
377
        $default = '';
×
378
        if ($randomDefault) {
×
379
            $question .= ' (Default: <i>Random</i>)';
×
380
            $default = Str::random(10, false);
×
381
        }
382

383
        return $this->ask($question, $default, true);
×
384
    }
385

386
    protected function confirmation(string $question = 'Are you sure?', bool $default = false): bool
387
    {
388
        $question     = '<yellow-r>Question:</yellow-r> ' . \trim($question);
×
389
        $defaultValue = $default ? 'Y' : 'n';
×
390
        $questionObj  = new ConfirmationQuestion(
×
391
            "{$question} (<c>Y/n</c>; Default: <i>{$defaultValue}</i>): ",
×
UNCOV
392
            $default,
×
UNCOV
393
        );
×
394

395
        return (bool)$this->getQuestionHelper()->ask(
×
396
            $this->outputMode->getInput(),
×
397
            $this->outputMode->getOutput(),
×
UNCOV
398
            $questionObj,
×
UNCOV
399
        );
×
400
    }
401

402
    /**
403
     * @param string[] $options
404
     */
405
    protected function askOption(string $question, array $options, null|float|int|string $default = null): string
406
    {
407
        $question = '<yellow-r>Question:</yellow-r> ' . \trim($question);
×
408

409
        $defaultValue = '';
×
410
        if ($default !== null) {
×
411
            /** @phpstan-ignore-next-line */
NEW
412
            $defaultValue = $options[$default] ?? '';
×
NEW
413
            if (!bool($default)) {
×
NEW
414
                $default = '';
×
415
            }
416

417
            if ($defaultValue !== '') {
×
418
                $defaultValue = " (Default: <i>{$defaultValue}</i>)";
×
419
            }
420
        }
421

422
        $questionObj = new ChoiceQuestion($question . $defaultValue . ': ', $options, $default);
×
423
        $questionObj->setErrorMessage('The option "%s" is undefined. See the avaialable options');
×
424

425
        return (string)$this->getQuestionHelper()->ask(
×
426
            $this->outputMode->getInput(),
×
427
            $this->outputMode->getOutput(),
×
UNCOV
428
            $questionObj,
×
UNCOV
429
        );
×
430
    }
431

432
    protected static function getStdIn(): ?string
433
    {
434
        static $result; // It can be read only once, so we save result as internal varaible
24✔
435

436
        if ($result === null) {
24✔
437
            $result = '';
24✔
438

439
            while (!\feof(\STDIN)) {
24✔
440
                $result .= \fread(\STDIN, 1024);
24✔
441
            }
442
        }
443

444
        return $result;
24✔
445
    }
446

447
    private function showLegacyOutput(string $echoContent): void
448
    {
449
        $lines = \explode("\n", $echoContent);
252✔
450
        $lines = \array_map(static fn ($line) => \rtrim($line), $lines);
252✔
451
        $lines = \array_filter($lines, static fn ($line): bool => $line !== '');
252✔
452

453
        if (\count($lines) > 1) {
252✔
454
            $this->_($lines, OutLvl::LEGACY);
252✔
455
        } else {
456
            $this->_($echoContent, OutLvl::LEGACY);
×
457
        }
458
    }
459

460
    private function getQuestionHelper(): QuestionHelper
461
    {
462
        $helper = $this->getHelper('question');
×
463
        if ($helper instanceof QuestionHelper) {
×
464
            return $helper;
×
465
        }
466

467
        throw new Exception('Symfony QuestionHelper not found');
×
468
    }
469

470
    private function createOutputMode(
471
        InputInterface $input,
472
        OutputInterface $output,
473
        string $outputFormat,
474
    ): AbstractOutputMode {
475
        $application = $this->getCliApplication();
876✔
476

477
        if ($outputFormat === Text::getName()) {
876✔
478
            return new Text($input, $output, $application);
612✔
479
        }
480

481
        if ($outputFormat === Cron::getName()) {
264✔
482
            return new Cron($input, $output, $application);
30✔
483
        }
484

485
        if ($outputFormat === Logstash::getName()) {
234✔
486
            return new Logstash($input, $output, $application);
234✔
487
        }
488

489
        throw new Exception("Unknown output format: {$outputFormat}");
×
490
    }
491

492
    private function getCliApplication(): CliApplication
493
    {
494
        $application = $this->getApplication();
876✔
495
        if ($application === null) {
876✔
496
            throw new Exception('Application not defined. Please, use "setApplication()" method.');
×
497
        }
498

499
        if ($application instanceof CliApplication) {
876✔
500
            return $application;
876✔
501
        }
502

503
        throw new Exception('Application must be instance of "\JBZoo\Cli\CliApplication"');
×
504
    }
505

506
    private static function getOutputFormat(InputInterface $input): string
507
    {
508
        if (bool($input->getOption('cron'))) { // TODO: Must be deprecated in the future
876✔
509
            return Cron::getName();
18✔
510
        }
511

512
        return $input->getOption('output-mode') ?? Text::getName();
858✔
513
    }
514
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc