Skip to content

Commit 051078b

Browse files
authored
feat(view): add meta command for view components (#1424)
1 parent 07f9f4d commit 051078b

File tree

9 files changed

+209
-3
lines changed

9 files changed

+209
-3
lines changed

packages/console/src/HasConsole.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,13 @@ public function keyValue(string $key, ?string $value = null): self
144144
return $this;
145145
}
146146

147+
public function header(string $header, ?string $subheader = null): self
148+
{
149+
$this->console->header($header, $subheader);
150+
151+
return $this;
152+
}
153+
147154
public function task(string $label, null|Process|Closure $handler): bool
148155
{
149156
return $this->console->task($label, $handler);

packages/support/src/Str/ManipulatesString.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -665,9 +665,9 @@ public function chunk(int $length): ImmutableArray
665665
/**
666666
* Explodes the string into an {@see \Tempest\Support\Arr\ImmutableArray} instance by a separator.
667667
*/
668-
public function explode(string $separator = ' '): ImmutableArray
668+
public function explode(string $separator = ' ', int $limit = PHP_INT_MAX): ImmutableArray
669669
{
670-
return new ImmutableArray(explode($separator, $this->value));
670+
return new ImmutableArray(explode($separator, $this->value, $limit));
671671
}
672672

673673
/**

packages/support/tests/Str/ManipulatesStringTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ public function test_explode(): void
360360
{
361361
$this->assertTrue(str('path/to/tempest')->explode('/')->equals(['path', 'to', 'tempest']));
362362
$this->assertTrue(str('john doe')->explode()->equals(['john', 'doe']));
363+
$this->assertTrue(str('john doe foo bar')->explode(limit: 2)->equals(['john', 'doe foo bar']));
363364
}
364365

365366
public function test_implode(): void

packages/view/src/Components/AnonymousViewComponent.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
final readonly class AnonymousViewComponent implements ViewComponent
1111
{
1212
public function __construct(
13+
public string $name,
1314
public string $contents,
1415
public string $file,
1516
) {}

packages/view/src/ViewComponentDiscovery.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ private function registerElementComponent(DiscoveryLocation $location, string $p
8484
$this->discoveryItems->add($location, [
8585
$name,
8686
new AnonymousViewComponent(
87+
name: $name,
8788
contents: $header . $view,
8889
file: $path,
8990
),
@@ -99,6 +100,7 @@ private function registerFileComponent(
99100
$this->discoveryItems->add($location, [
100101
$fileName->toString(),
101102
new AnonymousViewComponent(
103+
name: $fileName->toString(),
102104
contents: $contents->toString(),
103105
file: $path,
104106
),
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
namespace Tempest\Framework\Commands;
4+
5+
use Tempest\Console\ConsoleArgument;
6+
use Tempest\Console\ConsoleCommand;
7+
use Tempest\Console\HasConsole;
8+
use Tempest\Support\Arr\ImmutableArray;
9+
use Tempest\Support\Str\ImmutableString;
10+
use Tempest\View\Components\AnonymousViewComponent;
11+
use Tempest\View\Slot;
12+
use Tempest\View\ViewComponent;
13+
use Tempest\View\ViewConfig;
14+
15+
use function Tempest\Support\arr;
16+
use function Tempest\Support\Filesystem\is_file;
17+
use function Tempest\Support\str;
18+
19+
final class MetaViewComponentCommand
20+
{
21+
use HasConsole;
22+
23+
public function __construct(
24+
private readonly ViewConfig $viewConfig,
25+
) {}
26+
27+
#[ConsoleCommand(name: 'meta:view-component', hidden: true)]
28+
public function __invoke(
29+
#[ConsoleArgument(description: "The view component's name or the path to a view component file")]
30+
?string $viewComponent = null,
31+
): void {
32+
if ($viewComponent) {
33+
$viewComponentName = $viewComponent;
34+
35+
$viewComponent = $this->resolveViewComponent($viewComponentName);
36+
37+
if ($viewComponent === null) {
38+
$this->error('Unknown view component `' . $viewComponentName . '`');
39+
return;
40+
}
41+
42+
$data = $this->makeData($viewComponent);
43+
} else {
44+
$data = arr($this->viewConfig->viewComponents)
45+
->filter(fn (string|ViewComponent $viewComponent) => $viewComponent instanceof AnonymousViewComponent)
46+
->map(fn (AnonymousViewComponent $viewComponent) => $this->makeData($viewComponent)->toArray());
47+
}
48+
49+
$this->writeln($data->encodeJson(pretty: true));
50+
}
51+
52+
private function makeData(AnonymousViewComponent $viewComponent): ImmutableArray
53+
{
54+
return arr([
55+
'file' => $viewComponent->file,
56+
'name' => $viewComponent->name,
57+
'slots' => $this->resolveSlots($viewComponent)->toArray(),
58+
'variables' => $this->resolveVariables($viewComponent)->toArray(),
59+
]);
60+
}
61+
62+
private function resolveViewComponent(string $viewComponent): ?AnonymousViewComponent
63+
{
64+
if (is_file($viewComponent)) {
65+
foreach ($this->viewConfig->viewComponents as $registeredViewComponent) {
66+
if (! ($registeredViewComponent instanceof AnonymousViewComponent)) {
67+
continue;
68+
}
69+
70+
if ($registeredViewComponent->file !== $viewComponent) {
71+
continue;
72+
}
73+
74+
$viewComponent = $registeredViewComponent;
75+
76+
break;
77+
}
78+
} else {
79+
$viewComponent = $this->viewConfig->viewComponents[$viewComponent] ?? null;
80+
}
81+
82+
if ($viewComponent === null) {
83+
return null;
84+
}
85+
86+
if (! ($viewComponent instanceof AnonymousViewComponent)) {
87+
return null;
88+
}
89+
90+
return $viewComponent;
91+
}
92+
93+
private function resolveSlots(AnonymousViewComponent $viewComponent): ImmutableArray
94+
{
95+
preg_match_all('/<x-slot\s*(name="(?<name>[\w-]+)")?((\s*\/>)|>(?<default>(.|\n)*?)<\/x-slot>)/', $viewComponent->contents, $matches);
96+
97+
return arr($matches['name'])
98+
->mapWithKeys(fn (string $name) => yield $name => $name === '' ? Slot::DEFAULT : $name)
99+
->values();
100+
}
101+
102+
private function resolveVariables(AnonymousViewComponent $viewComponent): ImmutableArray
103+
{
104+
return str($viewComponent->contents)
105+
->matchAll('/^\s*\*\s*@var.*$/m')
106+
->map(fn (array $matches) => str($matches[0]))
107+
->map(fn (ImmutableString $line) => $line->replaceRegex('/^\s*\*\s*@var\s*/', ''))
108+
->map(fn (ImmutableString $line) => $line->trim())
109+
->map(fn (ImmutableString $line) => $line->explode(limit: 3))
110+
->mapWithKeys(
111+
fn (ImmutableArray $parts) => yield $parts[1] => [
112+
'type' => $parts[0],
113+
'name' => $parts[1],
114+
'attributeName' => str($parts[1])->kebab()->ltrim('$'),
115+
'description' => $parts[2] ?? null,
116+
],
117+
)
118+
->filter(fn (array $parts) => $parts['name'] !== '$this')
119+
->values();
120+
}
121+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
/**
3+
* @var string $title
4+
* @var \Tests\Tempest\Fixtures\Modules\Books\Models\Book $book Any kind of book will work
5+
* @var string $dataFoo
6+
*/
7+
?>
8+
9+
<div>
10+
<x-slot />
11+
<x-slot name="foo" />
12+
<x-slot name="bar" />
13+
</div>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace Integration\Framework\Commands;
4+
5+
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
6+
7+
final class MetaViewComponentCommandTest extends FrameworkIntegrationTestCase
8+
{
9+
public function test_show_meta_for_all_components(): void
10+
{
11+
$this->console
12+
->call('meta:view-component')
13+
->assertSuccess()
14+
->assertSee('"x-with-header"')
15+
->assertSee('"x-with-variable"')
16+
->assertSee(' "slots": [
17+
"other",
18+
"default"
19+
],
20+
')
21+
->assertNotSee('$this');
22+
}
23+
24+
public function test_show_meta_for_view_component(): void
25+
{
26+
$this->console
27+
->call('meta:view-component x-view-component-with-named-slots')
28+
->assertSuccess()
29+
->assertSee('x-view-component-with-named-slots.view.php')
30+
->assertSee('"name": "x-view-component-with-named-slots",')
31+
->assertSee(<<<'JSON'
32+
"variables": [
33+
{
34+
"type": "string",
35+
"name": "$title",
36+
"attributeName": "title",
37+
"description": null
38+
},
39+
{
40+
"type": "\\Tests\\Tempest\\Fixtures\\Modules\\Books\\Models\\Book",
41+
"name": "$book",
42+
"attributeName": "book",
43+
"description": "Any kind of book will work"
44+
},
45+
{
46+
"type": "string",
47+
"name": "$dataFoo",
48+
"attributeName": "data-foo",
49+
"description": null
50+
}
51+
]
52+
JSON)
53+
->assertSee(<<<'JSON'
54+
"slots": [
55+
"default",
56+
"foo",
57+
"bar"
58+
],
59+
JSON);
60+
}
61+
}

tests/Integration/FrameworkIntegrationTestCase.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ protected function render(string|View $view, mixed ...$params): string
108108

109109
protected function registerViewComponent(string $name, string $html): void
110110
{
111-
$viewComponent = new AnonymousViewComponent($html, '');
111+
$viewComponent = new AnonymousViewComponent($name, $html, '');
112112

113113
$this->container->get(ViewConfig::class)->addViewComponent($name, $viewComponent);
114114
}

0 commit comments

Comments
 (0)