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('