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

JBZoo / Cli / 7392476149

12 Dec 2023 10:40PM UTC coverage: 83.803% (-0.05%) from 83.848%
7392476149

push

github

web-flow
Feature flag to add `timestamp_real` (#23)

2 of 3 new or added lines in 1 file covered. (66.67%)

15 existing lines in 2 files now uncovered.

952 of 1136 relevant lines covered (83.8%)

138.74 hits per line

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

70.18
/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;
84✔
54

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

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

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

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

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

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

76
        return $progressBar;
132✔
77
    }
78

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

142
        parent::configure();
584✔
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));
584✔
151
        $this->getCliApplication()->setOutputMode($this->outputMode);
584✔
152

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

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

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

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

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

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

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

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

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

193
        return $exitCode;
496✔
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);
584✔
202

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

207
        return $value;
584✔
208
    }
209

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

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

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

228
        if (\count($onlyExpectedOptions) > 0 && !\in_array($result, $onlyExpectedOptions, true)) {
240✔
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);
240✔
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;
116✔
244
        $result = float($value);
116✔
245

246
        if (\count($onlyExpectedOptions) > 0 && !\in_array($result, $onlyExpectedOptions, true)) {
116✔
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;
116✔
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));
548✔
262
        $length = \strlen($value);
548✔
263
        $result = $length > 0 ? $value : $default;
548✔
264

265
        if (\count($onlyExpectedOptions) > 0 && !\in_array($result, $onlyExpectedOptions, true)) {
548✔
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;
548✔
273
    }
274

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

279
        return (array)$list;
116✔
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);
420✔
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();
584✔
338
        if ($application === null) {
584✔
339
            return 0;
×
340
        }
341

342
        if ($application instanceof CliApplication) {
584✔
343
            $eManager = $application->getEventManager();
584✔
344
            if ($eManager === null) {
584✔
345
                return 0;
584✔
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 */
412
            $defaultValue = $options[$default] ?? $default ?: '';
×
413
            if ($defaultValue !== '') {
×
414
                $defaultValue = " (Default: <i>{$defaultValue}</i>)";
×
415
            }
416
        }
417

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

421
        return (string)$this->getQuestionHelper()->ask(
×
422
            $this->outputMode->getInput(),
×
423
            $this->outputMode->getOutput(),
×
UNCOV
424
            $questionObj,
UNCOV
425
        );
426
    }
427

428
    protected static function getStdIn(): ?string
429
    {
430
        static $result; // It can be read only once, so we save result as internal varaible
8✔
431

432
        if ($result === null) {
16✔
433
            $result = '';
16✔
434

435
            while (!\feof(\STDIN)) {
16✔
436
                $result .= \fread(\STDIN, 1024);
16✔
437
            }
438
        }
439

440
        return $result;
16✔
441
    }
442

443
    private function showLegacyOutput(string $echoContent): void
444
    {
445
        $lines = \explode("\n", $echoContent);
168✔
446
        $lines = \array_map(static fn ($line) => \rtrim($line), $lines);
168✔
447
        $lines = \array_filter($lines, static fn ($line): bool => $line !== '');
168✔
448

449
        if (\count($lines) > 1) {
168✔
450
            $this->_($lines, OutLvl::LEGACY);
168✔
451
        } else {
452
            $this->_($echoContent, OutLvl::LEGACY);
×
453
        }
454
    }
455

456
    private function getQuestionHelper(): QuestionHelper
457
    {
458
        $helper = $this->getHelper('question');
×
459
        if ($helper instanceof QuestionHelper) {
×
460
            return $helper;
×
461
        }
462

463
        throw new Exception('Symfony QuestionHelper not found');
×
464
    }
465

466
    private function createOutputMode(
467
        InputInterface $input,
468
        OutputInterface $output,
469
        string $outputFormat,
470
    ): AbstractOutputMode {
471
        $application = $this->getCliApplication();
584✔
472

473
        if ($outputFormat === Text::getName()) {
584✔
474
            return new Text($input, $output, $application);
408✔
475
        }
476

477
        if ($outputFormat === Cron::getName()) {
176✔
478
            return new Cron($input, $output, $application);
20✔
479
        }
480

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

485
        throw new Exception("Unknown output format: {$outputFormat}");
×
486
    }
487

488
    private function getCliApplication(): CliApplication
489
    {
490
        $application = $this->getApplication();
584✔
491
        if ($application === null) {
584✔
492
            throw new Exception('Application not defined. Please, use "setApplication()" method.');
×
493
        }
494

495
        if ($application instanceof CliApplication) {
584✔
496
            return $application;
584✔
497
        }
498

499
        throw new Exception('Application must be instance of "\JBZoo\Cli\CliApplication"');
×
500
    }
501

502
    private static function getOutputFormat(InputInterface $input): string
503
    {
504
        if (bool($input->getOption('cron'))) { // TODO: Must be deprecated in the future
584✔
505
            return Cron::getName();
12✔
506
        }
507

508
        return $input->getOption('output-mode') ?? Text::getName();
572✔
509
    }
510
}
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