Skip to content

Commit 80ab136

Browse files
authored
feat(container): add container:show command (#1118)
1 parent 8d43ce5 commit 80ab136

File tree

3 files changed

+221
-0
lines changed

3 files changed

+221
-0
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
namespace Tempest\Container\Commands;
4+
5+
use Closure;
6+
use Tempest\Console\Console;
7+
use Tempest\Console\ConsoleCommand;
8+
use Tempest\Console\ExitCode;
9+
use Tempest\Container\Container;
10+
use Tempest\Container\GenericContainer;
11+
use Tempest\Reflection\ClassReflector;
12+
use Tempest\Reflection\FunctionReflector;
13+
14+
use function Tempest\Support\Arr\sort;
15+
use function Tempest\Support\Arr\sort_keys;
16+
use function Tempest\Support\str;
17+
use function Tempest\Support\Str\after_last;
18+
use function Tempest\Support\Str\before_last;
19+
use function Tempest\Support\Str\contains;
20+
21+
final readonly class ContainerShowCommand
22+
{
23+
public function __construct(
24+
private Container $container,
25+
private Console $console,
26+
) {}
27+
28+
#[ConsoleCommand(description: 'Shows the container bindings')]
29+
public function __invoke(): ExitCode
30+
{
31+
if (! ($this->container instanceof GenericContainer)) {
32+
$this->console->error('The registered container instance does not expose its bindings.');
33+
34+
return ExitCode::ERROR;
35+
}
36+
37+
$this->listBindings(
38+
title: 'Initializers',
39+
bindings: sort($this->container->getInitializers()),
40+
formatKey: fn (string $class): string => $this->formatClassKey($class),
41+
formatValue: fn (string $_, string $initializer): string => $this->formatClassValue($initializer),
42+
reject: static fn (string $_, string $initializer): bool => contains($initializer, ['\\Stubs', '\\Fixtures']),
43+
);
44+
45+
$this->listBindings(
46+
title: 'Dyanmic initializers',
47+
bindings: sort($this->container->getDynamicInitializers()),
48+
formatKey: fn (int $_, string $class): string => $this->formatClassKey($class),
49+
formatValue: static function (int $_, string $class): string {
50+
$name = new ClassReflector($class)
51+
->getMethod('initialize')
52+
->getReturnType()
53+
->getName();
54+
55+
return match ($name) {
56+
'object' => "<style='fg-gray'>object</style>",
57+
default => "<style='fg-blue'>{$name}</style>",
58+
};
59+
},
60+
);
61+
62+
$this->listBindings('Definitions', sort_keys($this->container->getDefinitions()));
63+
$this->listBindings('Singletons', sort_keys($this->container->getSingletons()));
64+
65+
return ExitCode::SUCCESS;
66+
}
67+
68+
private function listBindings(string $title, array $bindings, ?Closure $formatKey = null, ?Closure $formatValue = null, ?Closure $reject = null): void
69+
{
70+
if (! $bindings) {
71+
return;
72+
}
73+
74+
$reject ??= static fn (): bool => false;
75+
$formatKey ??= fn (int|string $key): string => $this->formatClassKey($key);
76+
$formatValue ??= fn (int|string $key, mixed $value): string => $this->formatClassValue($value, $key);
77+
78+
$this->console->header($title);
79+
80+
foreach ($bindings as $class => $definition) {
81+
if ($reject($class, $definition)) {
82+
continue;
83+
}
84+
85+
$this->console->keyValue(
86+
key: $formatKey($class, $definition),
87+
value: $formatValue($class, $definition),
88+
);
89+
}
90+
}
91+
92+
private function formatClassValue(string|object $class, mixed $key = null): string
93+
{
94+
if ($class instanceof Closure) {
95+
$serialized = str(new FunctionReflector($class)->getName())->afterFirst(':')->stripEnd('}');
96+
$declaringClass = $serialized->before('::');
97+
$method = $serialized->between('::', '():');
98+
$line = $serialized->afterLast(':');
99+
100+
return sprintf(
101+
"<style='fg-blue dim'>%s</style><style='dim'>::</style><style='fg-blue'>%s</style><style='dim'>():</style><style='fg-blue'>%s</style>",
102+
$declaringClass,
103+
$method,
104+
$line,
105+
);
106+
}
107+
108+
if (! is_string($class)) {
109+
$class = $class::class;
110+
}
111+
112+
if ($key === $class) {
113+
return "<style='fg-green dim bold'>SELF</style>";
114+
}
115+
116+
$namespace = before_last($class, '\\');
117+
$name = after_last($class, '\\');
118+
119+
return sprintf("<style='fg-blue dim'>%s\\</style><style='fg-blue'>%s</style>", $namespace, $name);
120+
}
121+
122+
private function formatClassKey(string $class): string
123+
{
124+
$namespace = before_last($class, '\\');
125+
$name = after_last($class, '\\');
126+
127+
return sprintf("<style='fg-gray'>%s\\</style>%s", $namespace, $name);
128+
}
129+
}

src/Tempest/Container/src/GenericContainer.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ public function getDefinitions(): array
6363
return $this->definitions->getArrayCopy();
6464
}
6565

66+
public function getSingletons(): array
67+
{
68+
return $this->singletons->getArrayCopy();
69+
}
70+
6671
public function getInitializers(): array
6772
{
6873
return $this->initializers->getArrayCopy();
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace Tests\Tempest\Integration\Container\Commands;
4+
5+
use Tempest\Container\Commands\ContainerShowCommand;
6+
use Tempest\Container\Container;
7+
use Tempest\Container\GenericContainer;
8+
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
9+
10+
final class ContainerShowCommandTest extends FrameworkIntegrationTestCase
11+
{
12+
public function test_command(): void
13+
{
14+
$this->console
15+
->call(ContainerShowCommand::class)
16+
->assertSee('INITIALIZERS')
17+
->assertSee('SINGLETONS')
18+
->assertSuccess();
19+
}
20+
21+
public function test_with_another_container(): void
22+
{
23+
$this->container->singleton(
24+
Container::class,
25+
new class(clone $this->container) implements Container {
26+
public function __construct(
27+
private readonly Container $container,
28+
) {}
29+
30+
public function register(string $className, callable $definition): self
31+
{
32+
$this->container->register($className, $definition);
33+
34+
return $this;
35+
}
36+
37+
public function unregister(string $className): self
38+
{
39+
$this->container->unregister($className);
40+
41+
return $this;
42+
}
43+
44+
public function singleton(string $className, mixed $definition, ?string $tag = null): self
45+
{
46+
$this->container->singleton($className, $definition, $tag);
47+
48+
return $this;
49+
}
50+
51+
public function config(object $config): self
52+
{
53+
$this->container->config($config);
54+
55+
return $this;
56+
}
57+
58+
public function get(string $className, ?string $tag = null, mixed ...$params): mixed
59+
{
60+
return $this->container->get($className, $tag, ...$params);
61+
}
62+
63+
public function has(string $className, ?string $tag = null): bool
64+
{
65+
return $this->container->has($className, $tag);
66+
}
67+
68+
public function invoke(mixed $method, mixed ...$params): mixed
69+
{
70+
return $this->container->invoke($method, ...$params);
71+
}
72+
73+
public function addInitializer(mixed $initializerClass): self
74+
{
75+
$this->container->addInitializer($initializerClass);
76+
77+
return $this;
78+
}
79+
},
80+
);
81+
82+
$this->console
83+
->call(ContainerShowCommand::class)
84+
->assertSee('The registered container instance does not expose its bindings.')
85+
->assertError();
86+
}
87+
}

0 commit comments

Comments
 (0)