Skip to content

Commit

Permalink
feat(framework): add config:show command (#732)
Browse files Browse the repository at this point in the history
  • Loading branch information
aazsamir authored Nov 14, 2024
1 parent 987eabf commit 2124577
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 25 deletions.
3 changes: 3 additions & 0 deletions src/Tempest/Console/src/Console.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Tempest\Console;

use Closure;
use Tempest\Highlight\Language;

interface Console
{
Expand All @@ -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
*/
Expand Down
18 changes: 13 additions & 5 deletions src/Tempest/Console/src/GenericConsole.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -95,11 +96,7 @@ public function readln(): string

public function write(string $contents): static
{
if ($this->label) {
$contents = "<h2>{$this->label}</h2> {$contents}";
}

$this->output->write($this->highlighter->parse($contents, new TempestConsoleLanguage()));
$this->writeWithLanguage($contents, new TempestConsoleLanguage());

return $this;
}
Expand All @@ -111,6 +108,17 @@ public function writeln(string $line = ''): static
return $this;
}

public function writeWithLanguage(string $contents, Language $language): Console
{
if ($this->label) {
$contents = "<h2>{$this->label}</h2> {$contents}";
}

$this->output->write($this->highlighter->parse($contents, $language));

return $this;
}

public function info(string $line): self
{
$this->writeln("<em>{$line}</em>");
Expand Down
7 changes: 7 additions & 0 deletions src/Tempest/Console/src/Testing/ConsoleTester.php
Original file line number Diff line number Diff line change
Expand Up @@ -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?");
Expand Down
46 changes: 26 additions & 20 deletions src/Tempest/Core/src/Kernel/LoadConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
}
210 changes: 210 additions & 0 deletions src/Tempest/Framework/Commands/ConfigShowCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<?php

declare(strict_types=1);

namespace Tempest\Framework\Commands;

use function file_get_contents;
use function function_exists;
use function is_array;
use function is_object;
use function realpath;
use function str_contains;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\ExitCode;
use Tempest\Console\HasConsole;
use Tempest\Console\Terminal\Terminal;
use Tempest\Core\Kernel\LoadConfig;
use Tempest\Highlight\Languages\Json\JsonLanguage;
use Tempest\Highlight\Languages\Php\PhpLanguage;
use Tempest\Reflection\ClassReflector;
use function var_export;

final readonly class ConfigShowCommand
{
use HasConsole;

private const int MAX_JSON_DEPTH = 32;

public function __construct(
private LoadConfig $loadConfig,
) {
}

#[ConsoleCommand(
name: 'config:show',
description: 'Show resolved configuration',
aliases: ['config'],
)]
public function __invoke(
ConfigShowFormat $format = ConfigShowFormat::PRETTY,
?bool $search = false,
?string $filter = null,
): ExitCode {
$configs = $this->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<string, mixed>
*/
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<string, mixed> $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<string, mixed> $configs
*/
private function dump(array $configs): void
{
if (function_exists('lw')) {
lw($configs);

return;
}

$this->console->writeln(var_export($configs, true));
}

/**
* @param array<string, mixed> $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<string, mixed> $configs
*/
private function file(array $configs): void
{
$phpLanguage = new PhpLanguage();

foreach (array_keys($configs) as $path) {
$this->console->writeln("<em>{$path}</em>");
$this->console->writeWithLanguage(
file_get_contents($path),
$phpLanguage,
);
$this->console->writeln();
}
}
}
15 changes: 15 additions & 0 deletions src/Tempest/Framework/Commands/ConfigShowFormat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Tempest\Framework\Commands;

/**
* @internal
*/
enum ConfigShowFormat: string
{
case DUMP = 'dump';
case PRETTY = 'pretty';
case FILE = 'file';
}
34 changes: 34 additions & 0 deletions tests/Integration/Framework/Commands/ConfigShowCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Tests\Tempest\Integration\Framework\Commands;

use Tests\Tempest\Integration\FrameworkIntegrationTestCase;

/**
* @internal
*/
final class ConfigShowCommandTest extends FrameworkIntegrationTestCase
{
public function test_it_shows_config_in_json_format(): void
{
$this->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('<?php');
}
}

0 comments on commit 2124577

Please # to comment.