Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
ValidatingCommand
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
4 / 4
10
100.00% covered (success)
100.00%
1 / 1
 askWithValidation
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 reloadableChoice
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 translate
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 normalizeInput
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace Hyde\Publications\Commands;
6
7use Hyde\Console\Concerns\Command;
8use Illuminate\Support\Facades\Validator;
9use RuntimeException;
10
11use function __;
12use function array_merge;
13use function implode;
14use function in_array;
15use function sprintf;
16use function trim;
17use function ucfirst;
18
19/**
20 * @internal An extended Command class that provides validation methods.
21 *
22 * @see \Hyde\Framework\Testing\Feature\ValidatingCommandTest
23 */
24class ValidatingCommand extends Command
25{
26    /** @var int How many times can the validation loop run? It is high enough to not affect normal usage. */
27    protected final const MAX_RETRIES = 30;
28
29    /**
30     * Ask for a CLI input value until we pass validation rules.
31     *
32     * @param  int  $retryCount  How many times has the question been asked?
33     *
34     * @throws RuntimeException If the validation fails after MAX_RETRIES attempts.
35     */
36    public function askWithValidation(
37        string $name,
38        string $question,
39        array $rules = [],
40        mixed $default = null,
41        int $retryCount = 0
42    ): string {
43        if ($retryCount >= self::MAX_RETRIES) {
44            // Prevent infinite loops that may happen due to the method's recursion.
45            // For example when running a command in tests or without interaction.
46            throw new RuntimeException(sprintf("Too many validation errors trying to validate '$name' with rules: [%s]", implode(', ', $rules)));
47        }
48
49        $answer = trim((string) $this->ask(ucfirst($question), $default));
50        $validator = Validator::make([$name => self::normalizeInput($answer, $rules)], [$name => $rules]);
51
52        if ($validator->passes()) {
53            return $answer;
54        }
55
56        foreach ($validator->errors()->all() as $error) {
57            $this->error($this->translate($name, $error));
58        }
59
60        return $this->askWithValidation($name, $question, $rules, $default, $retryCount + 1);
61    }
62
63    /** @param  callable<array<string>>  $options A function that returns an array of options. It will be re-run if the user hits selects the added 'reload' option. */
64    public function reloadableChoice(callable $options, string $question, string $reloadMessage = 'Reload options', bool $multiple = false): string|array
65    {
66        $reloadMessage = "<fg=bright-blue>[$reloadMessage]</>";
67        do {
68            $selection = $this->choice($question, array_merge([$reloadMessage], $options()), multiple: $multiple);
69        } while (in_array($reloadMessage, (array) $selection));
70
71        return $selection;
72    }
73
74    protected function translate(string $name, string $error): string
75    {
76        return __($error, [
77            'attribute' => $name,
78        ]);
79    }
80
81    protected static function normalizeInput(string $value, array $rules): bool|string
82    {
83        if (in_array('boolean', $rules)) {
84            // Since the Laravel validation rule requires booleans to be boolean, but the Symfony
85            // console input is a string, so we need to convert it so that it can be validated.
86            if ($value === 'true') {
87                return true;
88            }
89            if ($value === 'false') {
90                return false;
91            }
92        }
93
94        return $value;
95    }
96}