Lines
100.00%
18 / 18
Functions and Methods
100.00%
5 / 5
Classes and Traits
100.00%
1 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| ConfigDumpCommand | 100.00% | 18 / 18 | 100.00% | 5 / 5 | 16 | 100.00% | 1 / 1 | |||
| __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 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace Stewart\Runtime\Console; | |
| 6 | ||
| 7 | use Stewart\Contracts\Exception\StewartException; | |
| 8 | use Stewart\Runtime\Config\ConfigLoader; | |
| 9 | use Stewart\Support\Secret\SecretName; | |
| 10 | use Stewart\Support\Url\UrlRedactor; | |
| 11 | use Symfony\Component\Console\Attribute\AsCommand; | |
| 12 | use Symfony\Component\Console\Command\Command; | |
| 13 | use Symfony\Component\Console\Input\InputInterface; | |
| 14 | use Symfony\Component\Console\Input\InputOption; | |
| 15 | use Symfony\Component\Console\Output\OutputInterface; | |
| 16 | use Symfony\Component\Yaml\Yaml; | |
| 17 | ||
| 18 | #[AsCommand(name: self::NAME, description: 'Show the effective configuration after the environment is applied')] | |
| 19 | final 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 | } |