Lines 100.00% 26 / 26
Methods 100.00% 10 / 10
Classes 100.00% 1 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 configure 100.00% 2 / 2 100.00% 1 / 1 1
 execute 100.00% 8 / 8 100.00% 1 / 1 3
 maskSecrets 100.00% 6 / 6 100.00% 1 / 1 8
 isEmptyValue 100.00% 1 / 1 100.00% 1 / 1 3
 [Stewart\Runtime\Console\StewartCommand] findConfigOption 100.00% 1 / 1 100.00% 1 / 1 1
 [Stewart\Runtime\Console\StewartCommand] nonEmptyStringOption 100.00% 2 / 2 100.00% 1 / 1 3
 [Stewart\Runtime\Console\StewartCommand] writeMissingConfigFileNotice 100.00% 2 / 2 100.00% 1 / 1 2
 [Stewart\Runtime\Console\StewartCommand] writeNotice 100.00% 1 / 1 100.00% 1 / 1 2
 [Stewart\Runtime\Console\StewartCommand] readStringArgument 100.00% 2 / 2 100.00% 1 / 1 2
19final class ConfigDumpCommand extends StewartCommand
20{
21    public const string NAME = 'config:dump';
22
23    // Broad on purpose: app options are free-form and can hold API keys.
24    private const string MASK = '***';
25
26    private const int YAML_INLINE_DEPTH = 10;
27
28    public function __construct(private readonly ConfigLoader $config)
29    {
30        parent::__construct();
31    }
32
33    protected function configure(): void
34    {
35        parent::configure();
36        $this->addOption('show-secrets', null, InputOption::VALUE_NONE, 'Print credentials instead of masking them');
37    }
38
39    protected function execute(InputInterface $input, OutputInterface $output): int
40    {
41        try {
42            $processed = $this->config->buildValidatedConfigTree($this->findConfigOption($input));
43        } catch (StewartException $e) {
44            $output->writeln('<error>' . $e->getMessage() . '</error>');
45
46            return Command::FAILURE;
47        }
48
49        $this->writeMissingConfigFileNotice($output, $this->config->findMissingDefaultFile($this->findConfigOption($input)));
50        $values = $input->getOption('show-secrets') === true ? $processed : $this->maskSecrets($processed);
51
52        $output->write(Yaml::dump($values, self::YAML_INLINE_DEPTH, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE));
53
54        return Command::SUCCESS;
55    }
56
57    /**
58     * @param array<array-key, mixed> $values
59     * @return array<array-key, mixed>
60     */
61    private function maskSecrets(array $values): array
62    {
63        foreach ($values as $key => $value) {
64            $values[$key] = match (true) {
65                SecretName::looksSecret((string) $key) && !self::isEmptyValue($value) => self::MASK,
66                \is_array($value) => $this->maskSecrets($value),
67                // URL credentials (e.g. a reverse proxy) sit under keys the pattern does not match.
68                \is_string($value) && UrlRedactor::looksLikeUrl($value) => UrlRedactor::redactCredentials($value),
69                default => $value,
70            };
71        }
72
73        return $values;
74    }
75
76    private static function isEmptyValue(mixed $value): bool
77    {
78        return $value === null || $value === '' || $value === [];
79    }
80}

Inherited from Stewart\Runtime\Console\StewartCommand

20    protected function findConfigOption(InputInterface $input): ?string
21    {
22        return $this->nonEmptyStringOption($input, 'config');
23    }
25    protected function nonEmptyStringOption(InputInterface $input, string $name): ?string
26    {
27        $value = $input->getOption($name);
28
29        return \is_string($value) && $value !== '' ? $value : null;
30    }
32    protected function writeMissingConfigFileNotice(OutputInterface $output, ?string $missingConfigFile): void
33    {
34        if ($missingConfigFile !== null) {
35            $this->writeNotice($output, \sprintf('No config file at %s; using the environment only.', $missingConfigFile));
36        }
37    }
39    protected function writeNotice(OutputInterface $output, string $notice): void
40    {
41        ($output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output)->writeln('<comment>' . $notice . '</comment>');
42    }
44    protected function readStringArgument(InputInterface $input, string $name): string
45    {
46        $value = $input->getArgument($name);
47
48        return \is_string($value) ? $value : '';
49    }