Skip to content

Commit 10a1733

Browse files
Merge pull request #305 from laravel/blade-component-namespace
Blade component improvements
2 parents 69ce58e + 6403949 commit 10a1733

16 files changed

+882
-148
lines changed

generatable.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
"label": "Blade components",
5050
"features": [
5151
"link",
52-
"completion"
52+
"completion",
53+
"hover"
5354
]
5455
},
5556
{

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,12 @@
194194
"generated": true,
195195
"description": "Enable completion for Blade components."
196196
},
197+
"Laravel.bladeComponent.hover": {
198+
"type": "boolean",
199+
"default": true,
200+
"generated": true,
201+
"description": "Enable hover information for Blade components."
202+
},
197203
"Laravel.config.diagnostics": {
198204
"type": "boolean",
199205
"default": true,

php-templates/blade-components.php

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
<?php
2+
3+
$components = new class {
4+
protected $autoloaded = [];
5+
6+
protected $prefixes = [];
7+
8+
public function __construct()
9+
{
10+
$this->autoloaded = require base_path("vendor/composer/autoload_psr4.php");
11+
}
12+
13+
public function all()
14+
{
15+
$components = collect(array_merge(
16+
$this->getStandardClasses(),
17+
$this->getStandardViews(),
18+
$this->getNamespaced(),
19+
$this->getAnonymousNamespaced(),
20+
$this->getAnonymous(),
21+
$this->getAliases(),
22+
))->groupBy('key')->map(fn($items) => [
23+
'isVendor' => $items->first()['isVendor'],
24+
'paths' => $items->pluck('path')->values(),
25+
'props' => $items->pluck('props')->values()->filter()->flatMap(fn($i) => $i),
26+
]);
27+
28+
return [
29+
'components' => $components,
30+
'prefixes' => $this->prefixes,
31+
];
32+
}
33+
34+
protected function getStandardViews()
35+
{
36+
$path = resource_path('views/components');
37+
38+
return $this->findFiles($path, 'blade.php');
39+
}
40+
41+
protected function findFiles($path, $extension, $keyCallback = null)
42+
{
43+
if (!is_dir($path)) {
44+
return [];
45+
}
46+
47+
$files = \Symfony\Component\Finder\Finder::create()
48+
->files()
49+
->name("*." . $extension)
50+
->in($path);
51+
$components = [];
52+
$pathRealPath = realpath($path);
53+
54+
foreach ($files as $file) {
55+
$realPath = $file->getRealPath();
56+
57+
$key = \Illuminate\Support\Str::of($realPath)
58+
->replace($pathRealPath, '')
59+
->ltrim('/\\')
60+
->replace('.' . $extension, '')
61+
->replace(['/', '\\'], '.')
62+
->pipe(fn($str) => $this->handleIndexComponents($str));
63+
64+
$components[] = [
65+
"path" => LaravelVsCode::relativePath($realPath),
66+
"isVendor" => LaravelVsCode::isVendor($realPath),
67+
"key" => $keyCallback ? $keyCallback($key) : $key,
68+
];
69+
}
70+
71+
return $components;
72+
}
73+
74+
protected function getStandardClasses()
75+
{
76+
$path = app_path('View/Components');
77+
78+
$appNamespace = collect($this->autoloaded)
79+
->filter(fn($paths) => in_array(app_path(), $paths))
80+
->keys()
81+
->first() ?? '';
82+
83+
return collect($this->findFiles(
84+
$path,
85+
'php',
86+
fn($key) => $key->explode('.')
87+
->map(fn($p) => \Illuminate\Support\Str::kebab($p))
88+
->implode('.'),
89+
))->map(function ($item) use ($appNamespace) {
90+
$class = \Illuminate\Support\Str::of($item['path'])
91+
->after('View/Components/')
92+
->replace('.php', '')
93+
->prepend($appNamespace . 'View\\Components\\')
94+
->toString();
95+
96+
if (!class_exists($class)) {
97+
return $item;
98+
}
99+
100+
$reflection = new \ReflectionClass($class);
101+
$parameters = collect($reflection->getConstructor()->getParameters())
102+
->filter(fn($p) => $p->isPromoted())
103+
->flatMap(fn($p) => [$p->getName() => $p->isOptional() ? $p->getDefaultValue() : null])
104+
->all();
105+
106+
$props = collect($reflection->getProperties())
107+
->filter(fn($p) => $p->isPublic() && $p->getDeclaringClass()->getName() === $class)
108+
->map(fn($p) => [
109+
'name' => \Illuminate\Support\Str::kebab($p->getName()),
110+
'type' => (string) ($p->getType() ?? 'mixed'),
111+
'default' => $p->getDefaultValue() ?? $parameters[$p->getName()] ?? null,
112+
]);
113+
114+
[$except, $props] = $props->partition(fn($p) => $p['name'] === 'except');
115+
116+
if ($except->isNotEmpty()) {
117+
$except = $except->first()['default'];
118+
$props = $props->reject(fn($p) => in_array($p['name'], $except));
119+
}
120+
121+
return [
122+
...$item,
123+
'props' => $props,
124+
];
125+
})->all();
126+
}
127+
128+
protected function getAliases()
129+
{
130+
$components = [];
131+
132+
foreach (\Illuminate\Support\Facades\Blade::getClassComponentAliases() as $key => $class) {
133+
if (class_exists($class)) {
134+
$reflection = new ReflectionClass($class);
135+
136+
$components[] = [
137+
"path" => LaravelVsCode::relativePath($reflection->getFileName()),
138+
"isVendor" => LaravelVsCode::isVendor($reflection->getFileName()),
139+
"key" => $key,
140+
];
141+
}
142+
}
143+
144+
return $components;
145+
}
146+
147+
protected function getAnonymousNamespaced()
148+
{
149+
$components = [];
150+
151+
foreach (\Illuminate\Support\Facades\Blade::getAnonymousComponentNamespaces() as $key => $dir) {
152+
$path = collect([$dir, resource_path('views/' . $dir)])->first(fn($p) => is_dir($p));
153+
154+
if (!$path) {
155+
continue;
156+
}
157+
158+
array_push(
159+
$components,
160+
...$this->findFiles(
161+
$path,
162+
'blade.php',
163+
fn($k) => $k->kebab()->prepend($key . "::"),
164+
)
165+
);
166+
}
167+
168+
return $components;
169+
}
170+
171+
protected function getAnonymous()
172+
{
173+
$components = [];
174+
175+
foreach (\Illuminate\Support\Facades\Blade::getAnonymousComponentPaths() as $item) {
176+
array_push(
177+
$components,
178+
...$this->findFiles(
179+
$item['path'],
180+
'blade.php',
181+
fn($key) => $key
182+
->kebab()
183+
->prepend(($item['prefix'] ?? ':') . ':')
184+
->ltrim(':'),
185+
)
186+
);
187+
188+
if (!in_array($item['prefix'], $this->prefixes)) {
189+
$this->prefixes[] = $item['prefix'];
190+
}
191+
}
192+
193+
return $components;
194+
}
195+
196+
protected function handleIndexComponents($str)
197+
{
198+
if ($str->endsWith('.index')) {
199+
return $str->replaceLast('.index', '');
200+
}
201+
202+
if (!$str->contains('.')) {
203+
return $str;
204+
}
205+
206+
$parts = $str->explode('.');
207+
208+
if ($parts->slice(-2)->unique()->count() === 1) {
209+
$parts->pop();
210+
211+
return \Illuminate\Support\Str::of($parts->implode('.'));
212+
}
213+
214+
return $str;
215+
}
216+
217+
protected function getNamespaced()
218+
{
219+
$namespaced = \Illuminate\Support\Facades\Blade::getClassComponentNamespaces();
220+
$components = [];
221+
222+
foreach ($namespaced as $key => $classNamespace) {
223+
$path = $this->getNamespacePath($classNamespace);
224+
225+
if (!$path) {
226+
continue;
227+
}
228+
229+
array_push(
230+
$components,
231+
...$this->findFiles(
232+
$path,
233+
'php',
234+
fn($k) => $k->kebab()->prepend($key . "::"),
235+
)
236+
);
237+
}
238+
239+
return $components;
240+
}
241+
242+
protected function getNamespacePath($classNamespace)
243+
{
244+
foreach ($this->autoloaded as $ns => $paths) {
245+
if (!str_starts_with($classNamespace, $ns)) {
246+
continue;
247+
}
248+
249+
foreach ($paths as $p) {
250+
$dir = \Illuminate\Support\Str::of($classNamespace)
251+
->replace($ns, '')
252+
->replace('\\', '/')
253+
->prepend($p . DIRECTORY_SEPARATOR)
254+
->toString();
255+
256+
if (is_dir($dir)) {
257+
return $dir;
258+
}
259+
}
260+
261+
return null;
262+
}
263+
264+
return null;
265+
}
266+
};
267+
268+
echo json_encode($components->all());

php-templates/bootstrap-laravel.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ public static function relativePath($path)
1717
return ltrim(str_replace(base_path(), '', realpath($path)), DIRECTORY_SEPARATOR);
1818
}
1919

20+
public static function isVendor($path)
21+
{
22+
return str_contains($path, base_path("vendor"));
23+
}
24+
2025
public static function outputMarker($key)
2126
{
2227
return '__VSCODE_LARAVEL_' . $key . '__';

0 commit comments

Comments
 (0)