Skip to content

Commit 2124577

Browse files
authored
feat(framework): add config:show command (#732)
1 parent 987eabf commit 2124577

File tree

7 files changed

+308
-25
lines changed

7 files changed

+308
-25
lines changed

src/Tempest/Console/src/Console.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Tempest\Console;
66

77
use Closure;
8+
use Tempest\Highlight\Language;
89

910
interface Console
1011
{
@@ -18,6 +19,8 @@ public function write(string $contents): self;
1819

1920
public function writeln(string $line = ''): self;
2021

22+
public function writeWithLanguage(string $contents, Language $language): self;
23+
2124
/**
2225
* @param \Tempest\Validation\Rule[] $validation
2326
*/

src/Tempest/Console/src/GenericConsole.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Tempest\Console\Input\ConsoleArgumentBag;
2020
use Tempest\Container\Tag;
2121
use Tempest\Highlight\Highlighter;
22+
use Tempest\Highlight\Language;
2223

2324
final class GenericConsole implements Console
2425
{
@@ -95,11 +96,7 @@ public function readln(): string
9596

9697
public function write(string $contents): static
9798
{
98-
if ($this->label) {
99-
$contents = "<h2>{$this->label}</h2> {$contents}";
100-
}
101-
102-
$this->output->write($this->highlighter->parse($contents, new TempestConsoleLanguage()));
99+
$this->writeWithLanguage($contents, new TempestConsoleLanguage());
103100

104101
return $this;
105102
}
@@ -111,6 +108,17 @@ public function writeln(string $line = ''): static
111108
return $this;
112109
}
113110

111+
public function writeWithLanguage(string $contents, Language $language): Console
112+
{
113+
if ($this->label) {
114+
$contents = "<h2>{$this->label}</h2> {$contents}";
115+
}
116+
117+
$this->output->write($this->highlighter->parse($contents, $language));
118+
119+
return $this;
120+
}
121+
114122
public function info(string $line): self
115123
{
116124
$this->writeln("<em>{$line}</em>");

src/Tempest/Console/src/Testing/ConsoleTester.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ public function assertContainsFormattedText(string $text): self
243243
return $this;
244244
}
245245

246+
public function assertJson(): self
247+
{
248+
Assert::assertJson($this->output->asUnformattedString());
249+
250+
return $this;
251+
}
252+
246253
public function assertExitCode(ExitCode $exitCode): self
247254
{
248255
Assert::assertNotNull($this->exitCode, "Expected {$exitCode->name}, but instead no exit code was set — maybe you missed providing some input?");

src/Tempest/Core/src/Kernel/LoadConfig.php

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,7 @@ public function __invoke(): void
2424
{
2525
$configPaths = $this->cache->resolve(
2626
'config_cache',
27-
function () {
28-
$configPaths = [];
29-
30-
// Scan for config files in all discovery locations
31-
foreach ($this->kernel->discoveryLocations as $discoveryLocation) {
32-
$directories = new RecursiveDirectoryIterator($discoveryLocation->path, FilesystemIterator::UNIX_PATHS | FilesystemIterator::SKIP_DOTS);
33-
$files = new RecursiveIteratorIterator($directories);
34-
35-
/** @var SplFileInfo $file */
36-
foreach ($files as $file) {
37-
if (! str_ends_with($file->getPathname(), '.config.php')) {
38-
continue;
39-
}
40-
41-
$configPaths[] = $file->getPathname();
42-
}
43-
}
44-
45-
return $configPaths;
46-
}
27+
fn () => $this->find()
4728
);
4829

4930
foreach ($configPaths as $path) {
@@ -52,4 +33,29 @@ function () {
5233
$this->kernel->container->config($configFile);
5334
}
5435
}
36+
37+
/**
38+
* @return string[]
39+
*/
40+
public function find(): array
41+
{
42+
$configPaths = [];
43+
44+
// Scan for config files in all discovery locations
45+
foreach ($this->kernel->discoveryLocations as $discoveryLocation) {
46+
$directories = new RecursiveDirectoryIterator($discoveryLocation->path, FilesystemIterator::UNIX_PATHS | FilesystemIterator::SKIP_DOTS);
47+
$files = new RecursiveIteratorIterator($directories);
48+
49+
/** @var SplFileInfo $file */
50+
foreach ($files as $file) {
51+
if (! str_ends_with($file->getPathname(), '.config.php')) {
52+
continue;
53+
}
54+
55+
$configPaths[] = $file->getPathname();
56+
}
57+
}
58+
59+
return $configPaths;
60+
}
5561
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Framework\Commands;
6+
7+
use function file_get_contents;
8+
use function function_exists;
9+
use function is_array;
10+
use function is_object;
11+
use function realpath;
12+
use function str_contains;
13+
use Tempest\Console\ConsoleCommand;
14+
use Tempest\Console\ExitCode;
15+
use Tempest\Console\HasConsole;
16+
use Tempest\Console\Terminal\Terminal;
17+
use Tempest\Core\Kernel\LoadConfig;
18+
use Tempest\Highlight\Languages\Json\JsonLanguage;
19+
use Tempest\Highlight\Languages\Php\PhpLanguage;
20+
use Tempest\Reflection\ClassReflector;
21+
use function var_export;
22+
23+
final readonly class ConfigShowCommand
24+
{
25+
use HasConsole;
26+
27+
private const int MAX_JSON_DEPTH = 32;
28+
29+
public function __construct(
30+
private LoadConfig $loadConfig,
31+
) {
32+
}
33+
34+
#[ConsoleCommand(
35+
name: 'config:show',
36+
description: 'Show resolved configuration',
37+
aliases: ['config'],
38+
)]
39+
public function __invoke(
40+
ConfigShowFormat $format = ConfigShowFormat::PRETTY,
41+
?bool $search = false,
42+
?string $filter = null,
43+
): ExitCode {
44+
$configs = $this->resolveConfig($filter, $search);
45+
46+
if (empty($configs)) {
47+
$this->console->error('No configuration found');
48+
49+
return ExitCode::ERROR;
50+
}
51+
52+
match ($format) {
53+
ConfigShowFormat::DUMP => $this->dump($configs),
54+
ConfigShowFormat::PRETTY => $this->pretty($configs),
55+
ConfigShowFormat::FILE => $this->file($configs),
56+
};
57+
58+
return ExitCode::SUCCESS;
59+
}
60+
61+
/**
62+
* @return array<string, mixed>
63+
*/
64+
private function resolveConfig(?string $filter, bool $search): array
65+
{
66+
$configPaths = $this->loadConfig->find();
67+
$configs = [];
68+
$uniqueMap = [];
69+
70+
foreach ($configPaths as $configPath) {
71+
$config = require $configPath;
72+
$configPath = realpath($configPath);
73+
74+
if (
75+
$filter === null
76+
|| str_contains($configPath, $filter)
77+
|| str_contains($config::class, $filter)
78+
) {
79+
$configs[$configPath] = $config;
80+
$uniqueMap[$config::class] = $configPath;
81+
}
82+
}
83+
84+
// LoadConfig::find() returns all config paths
85+
// that are overwritten by container in their order
86+
$resolvedConfigs = [];
87+
88+
foreach ($uniqueMap as $configPath) {
89+
$resolvedConfigs[$configPath] = $configs[$configPath];
90+
}
91+
92+
if (! $search) {
93+
return $resolvedConfigs;
94+
}
95+
96+
$selectedPath = $this->search($resolvedConfigs);
97+
98+
return [$selectedPath => $resolvedConfigs[$selectedPath]];
99+
}
100+
101+
/**
102+
* @param array<string, mixed> $configs
103+
*/
104+
private function search(array $configs): string
105+
{
106+
$data = array_keys($configs);
107+
sort($data);
108+
109+
$return = $this->console->search(
110+
label: 'Which configuration file would you like to view?',
111+
search: function (string $query) use ($data): array {
112+
if ($query === '') {
113+
return $data;
114+
}
115+
116+
return array_filter(
117+
array: $data,
118+
callback: fn (string $path) => str_contains($path, $query),
119+
);
120+
},
121+
default: $data[0],
122+
);
123+
124+
// TODO: This is a workaround for SearchComponent not clearing the terminal properly
125+
$terminal = new Terminal($this->console);
126+
$terminal->cursor->clearAfter();
127+
128+
return $return;
129+
}
130+
131+
/**
132+
* @param array<string, mixed> $configs
133+
*/
134+
private function dump(array $configs): void
135+
{
136+
if (function_exists('lw')) {
137+
lw($configs);
138+
139+
return;
140+
}
141+
142+
$this->console->writeln(var_export($configs, true));
143+
}
144+
145+
/**
146+
* @param array<string, mixed> $configs
147+
*/
148+
private function pretty(array $configs): void
149+
{
150+
$formatted = $this->formatForJson($configs);
151+
152+
$this->console->writeWithLanguage(
153+
json_encode(
154+
$formatted,
155+
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
156+
),
157+
new JsonLanguage(),
158+
);
159+
}
160+
161+
private function formatForJson(mixed $value, int $depth = 0): mixed
162+
{
163+
if ($depth > self::MAX_JSON_DEPTH) {
164+
return '@...';
165+
}
166+
167+
if (is_object($value)) {
168+
$result = [
169+
'@type' => $value::class,
170+
];
171+
172+
$reflector = new ClassReflector($value);
173+
174+
foreach ($reflector->getProperties() as $property) {
175+
$result[$property->getName()] = $this->formatForJson($property->getValue($value), $depth + 1);
176+
}
177+
178+
return $result;
179+
}
180+
181+
if (is_array($value)) {
182+
$result = [];
183+
184+
foreach ($value as $key => $item) {
185+
$result[$key] = $this->formatForJson($item, $depth + 1);
186+
}
187+
188+
return $result;
189+
}
190+
191+
return $value;
192+
}
193+
194+
/**
195+
* @param array<string, mixed> $configs
196+
*/
197+
private function file(array $configs): void
198+
{
199+
$phpLanguage = new PhpLanguage();
200+
201+
foreach (array_keys($configs) as $path) {
202+
$this->console->writeln("<em>{$path}</em>");
203+
$this->console->writeWithLanguage(
204+
file_get_contents($path),
205+
$phpLanguage,
206+
);
207+
$this->console->writeln();
208+
}
209+
}
210+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Framework\Commands;
6+
7+
/**
8+
* @internal
9+
*/
10+
enum ConfigShowFormat: string
11+
{
12+
case DUMP = 'dump';
13+
case PRETTY = 'pretty';
14+
case FILE = 'file';
15+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Integration\Framework\Commands;
6+
7+
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
8+
9+
/**
10+
* @internal
11+
*/
12+
final class ConfigShowCommandTest extends FrameworkIntegrationTestCase
13+
{
14+
public function test_it_shows_config_in_json_format(): void
15+
{
16+
$this->console
17+
->call('config:show --format=pretty --filter=database.config.php')
18+
->assertJson()
19+
->assertContains('database.config.php')
20+
->assertContains('DatabaseConfig')
21+
->assertDoesNotContain('views.config.php')
22+
->assertContains('@type');
23+
}
24+
25+
public function test_it_shows_config_in_file_format(): void
26+
{
27+
$this->console
28+
->call('config:show --format=file --filter=database.config.php')
29+
->assertContains('database.config.php')
30+
->assertContains('DatabaseConfig')
31+
->assertDoesNotContain('views.config.php')
32+
->assertContains('<?php');
33+
}
34+
}

0 commit comments

Comments
 (0)