diff --git a/src/Tempest/Console/src/Console.php b/src/Tempest/Console/src/Console.php
index caa54a50c..f5d6ffac1 100644
--- a/src/Tempest/Console/src/Console.php
+++ b/src/Tempest/Console/src/Console.php
@@ -5,6 +5,7 @@
namespace Tempest\Console;
use Closure;
+use Tempest\Highlight\Language;
interface Console
{
@@ -18,6 +19,8 @@ public function write(string $contents): self;
public function writeln(string $line = ''): self;
+ public function writeWithLanguage(string $contents, Language $language): self;
+
/**
* @param \Tempest\Validation\Rule[] $validation
*/
diff --git a/src/Tempest/Console/src/GenericConsole.php b/src/Tempest/Console/src/GenericConsole.php
index 7db2be083..f5f8e11de 100644
--- a/src/Tempest/Console/src/GenericConsole.php
+++ b/src/Tempest/Console/src/GenericConsole.php
@@ -19,6 +19,7 @@
use Tempest\Console\Input\ConsoleArgumentBag;
use Tempest\Container\Tag;
use Tempest\Highlight\Highlighter;
+use Tempest\Highlight\Language;
final class GenericConsole implements Console
{
@@ -95,11 +96,7 @@ public function readln(): string
public function write(string $contents): static
{
- if ($this->label) {
- $contents = "
{$this->label}
{$contents}";
- }
-
- $this->output->write($this->highlighter->parse($contents, new TempestConsoleLanguage()));
+ $this->writeWithLanguage($contents, new TempestConsoleLanguage());
return $this;
}
@@ -111,6 +108,17 @@ public function writeln(string $line = ''): static
return $this;
}
+ public function writeWithLanguage(string $contents, Language $language): Console
+ {
+ if ($this->label) {
+ $contents = "{$this->label}
{$contents}";
+ }
+
+ $this->output->write($this->highlighter->parse($contents, $language));
+
+ return $this;
+ }
+
public function info(string $line): self
{
$this->writeln("{$line}");
diff --git a/src/Tempest/Console/src/Testing/ConsoleTester.php b/src/Tempest/Console/src/Testing/ConsoleTester.php
index 89ef8b833..effb93f06 100644
--- a/src/Tempest/Console/src/Testing/ConsoleTester.php
+++ b/src/Tempest/Console/src/Testing/ConsoleTester.php
@@ -243,6 +243,13 @@ public function assertContainsFormattedText(string $text): self
return $this;
}
+ public function assertJson(): self
+ {
+ Assert::assertJson($this->output->asUnformattedString());
+
+ return $this;
+ }
+
public function assertExitCode(ExitCode $exitCode): self
{
Assert::assertNotNull($this->exitCode, "Expected {$exitCode->name}, but instead no exit code was set — maybe you missed providing some input?");
diff --git a/src/Tempest/Core/src/Kernel/LoadConfig.php b/src/Tempest/Core/src/Kernel/LoadConfig.php
index b53da3140..c2a16aa18 100644
--- a/src/Tempest/Core/src/Kernel/LoadConfig.php
+++ b/src/Tempest/Core/src/Kernel/LoadConfig.php
@@ -24,26 +24,7 @@ public function __invoke(): void
{
$configPaths = $this->cache->resolve(
'config_cache',
- function () {
- $configPaths = [];
-
- // Scan for config files in all discovery locations
- foreach ($this->kernel->discoveryLocations as $discoveryLocation) {
- $directories = new RecursiveDirectoryIterator($discoveryLocation->path, FilesystemIterator::UNIX_PATHS | FilesystemIterator::SKIP_DOTS);
- $files = new RecursiveIteratorIterator($directories);
-
- /** @var SplFileInfo $file */
- foreach ($files as $file) {
- if (! str_ends_with($file->getPathname(), '.config.php')) {
- continue;
- }
-
- $configPaths[] = $file->getPathname();
- }
- }
-
- return $configPaths;
- }
+ fn () => $this->find()
);
foreach ($configPaths as $path) {
@@ -52,4 +33,29 @@ function () {
$this->kernel->container->config($configFile);
}
}
+
+ /**
+ * @return string[]
+ */
+ public function find(): array
+ {
+ $configPaths = [];
+
+ // Scan for config files in all discovery locations
+ foreach ($this->kernel->discoveryLocations as $discoveryLocation) {
+ $directories = new RecursiveDirectoryIterator($discoveryLocation->path, FilesystemIterator::UNIX_PATHS | FilesystemIterator::SKIP_DOTS);
+ $files = new RecursiveIteratorIterator($directories);
+
+ /** @var SplFileInfo $file */
+ foreach ($files as $file) {
+ if (! str_ends_with($file->getPathname(), '.config.php')) {
+ continue;
+ }
+
+ $configPaths[] = $file->getPathname();
+ }
+ }
+
+ return $configPaths;
+ }
}
diff --git a/src/Tempest/Framework/Commands/ConfigShowCommand.php b/src/Tempest/Framework/Commands/ConfigShowCommand.php
new file mode 100644
index 000000000..ce0c2f1a1
--- /dev/null
+++ b/src/Tempest/Framework/Commands/ConfigShowCommand.php
@@ -0,0 +1,210 @@
+resolveConfig($filter, $search);
+
+ if (empty($configs)) {
+ $this->console->error('No configuration found');
+
+ return ExitCode::ERROR;
+ }
+
+ match ($format) {
+ ConfigShowFormat::DUMP => $this->dump($configs),
+ ConfigShowFormat::PRETTY => $this->pretty($configs),
+ ConfigShowFormat::FILE => $this->file($configs),
+ };
+
+ return ExitCode::SUCCESS;
+ }
+
+ /**
+ * @return array
+ */
+ private function resolveConfig(?string $filter, bool $search): array
+ {
+ $configPaths = $this->loadConfig->find();
+ $configs = [];
+ $uniqueMap = [];
+
+ foreach ($configPaths as $configPath) {
+ $config = require $configPath;
+ $configPath = realpath($configPath);
+
+ if (
+ $filter === null
+ || str_contains($configPath, $filter)
+ || str_contains($config::class, $filter)
+ ) {
+ $configs[$configPath] = $config;
+ $uniqueMap[$config::class] = $configPath;
+ }
+ }
+
+ // LoadConfig::find() returns all config paths
+ // that are overwritten by container in their order
+ $resolvedConfigs = [];
+
+ foreach ($uniqueMap as $configPath) {
+ $resolvedConfigs[$configPath] = $configs[$configPath];
+ }
+
+ if (! $search) {
+ return $resolvedConfigs;
+ }
+
+ $selectedPath = $this->search($resolvedConfigs);
+
+ return [$selectedPath => $resolvedConfigs[$selectedPath]];
+ }
+
+ /**
+ * @param array $configs
+ */
+ private function search(array $configs): string
+ {
+ $data = array_keys($configs);
+ sort($data);
+
+ $return = $this->console->search(
+ label: 'Which configuration file would you like to view?',
+ search: function (string $query) use ($data): array {
+ if ($query === '') {
+ return $data;
+ }
+
+ return array_filter(
+ array: $data,
+ callback: fn (string $path) => str_contains($path, $query),
+ );
+ },
+ default: $data[0],
+ );
+
+ // TODO: This is a workaround for SearchComponent not clearing the terminal properly
+ $terminal = new Terminal($this->console);
+ $terminal->cursor->clearAfter();
+
+ return $return;
+ }
+
+ /**
+ * @param array $configs
+ */
+ private function dump(array $configs): void
+ {
+ if (function_exists('lw')) {
+ lw($configs);
+
+ return;
+ }
+
+ $this->console->writeln(var_export($configs, true));
+ }
+
+ /**
+ * @param array $configs
+ */
+ private function pretty(array $configs): void
+ {
+ $formatted = $this->formatForJson($configs);
+
+ $this->console->writeWithLanguage(
+ json_encode(
+ $formatted,
+ JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
+ ),
+ new JsonLanguage(),
+ );
+ }
+
+ private function formatForJson(mixed $value, int $depth = 0): mixed
+ {
+ if ($depth > self::MAX_JSON_DEPTH) {
+ return '@...';
+ }
+
+ if (is_object($value)) {
+ $result = [
+ '@type' => $value::class,
+ ];
+
+ $reflector = new ClassReflector($value);
+
+ foreach ($reflector->getProperties() as $property) {
+ $result[$property->getName()] = $this->formatForJson($property->getValue($value), $depth + 1);
+ }
+
+ return $result;
+ }
+
+ if (is_array($value)) {
+ $result = [];
+
+ foreach ($value as $key => $item) {
+ $result[$key] = $this->formatForJson($item, $depth + 1);
+ }
+
+ return $result;
+ }
+
+ return $value;
+ }
+
+ /**
+ * @param array $configs
+ */
+ private function file(array $configs): void
+ {
+ $phpLanguage = new PhpLanguage();
+
+ foreach (array_keys($configs) as $path) {
+ $this->console->writeln("{$path}");
+ $this->console->writeWithLanguage(
+ file_get_contents($path),
+ $phpLanguage,
+ );
+ $this->console->writeln();
+ }
+ }
+}
diff --git a/src/Tempest/Framework/Commands/ConfigShowFormat.php b/src/Tempest/Framework/Commands/ConfigShowFormat.php
new file mode 100644
index 000000000..6eefd0d1c
--- /dev/null
+++ b/src/Tempest/Framework/Commands/ConfigShowFormat.php
@@ -0,0 +1,15 @@
+console
+ ->call('config:show --format=pretty --filter=database.config.php')
+ ->assertJson()
+ ->assertContains('database.config.php')
+ ->assertContains('DatabaseConfig')
+ ->assertDoesNotContain('views.config.php')
+ ->assertContains('@type');
+ }
+
+ public function test_it_shows_config_in_file_format(): void
+ {
+ $this->console
+ ->call('config:show --format=file --filter=database.config.php')
+ ->assertContains('database.config.php')
+ ->assertContains('DatabaseConfig')
+ ->assertDoesNotContain('views.config.php')
+ ->assertContains('