Skip to content

Commit 5a1eb8b

Browse files
authored
Anonymous component paths (#45338)
* allow registration of other anonymous component paths * add test * check for delimiter and bail early if possible * fix test * add index test
1 parent 684a512 commit 5a1eb8b

File tree

3 files changed

+150
-13
lines changed

3 files changed

+150
-13
lines changed

src/Illuminate/View/Compilers/BladeCompiler.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ class BladeCompiler extends Compiler implements CompilerInterface
123123
*/
124124
protected $rawBlocks = [];
125125

126+
/**
127+
* The array of anonymous component paths to search for components in.
128+
*
129+
* @var array
130+
*/
131+
protected $anonymousComponentPaths = [];
132+
126133
/**
127134
* The array of anonymous component namespaces to autoload from.
128135
*
@@ -682,6 +689,21 @@ public function getClassComponentAliases()
682689
return $this->classComponentAliases;
683690
}
684691

692+
/**
693+
* Register a new anonymous component path.
694+
*
695+
* @param string $path
696+
* @return void
697+
*/
698+
public function anonymousComponentPath(string $path)
699+
{
700+
$this->anonymousComponentPaths[] = $path;
701+
702+
Container::getInstance()
703+
->make(ViewFactory::class)
704+
->addNamespace(md5($path), $path);
705+
}
706+
685707
/**
686708
* Register an anonymous component namespace.
687709
*
@@ -711,6 +733,16 @@ public function componentNamespace($namespace, $prefix)
711733
$this->classComponentNamespaces[$prefix] = $namespace;
712734
}
713735

736+
/**
737+
* Get the registered anonymous component paths.
738+
*
739+
* @return array
740+
*/
741+
public function getAnonymousComponentPaths()
742+
{
743+
return $this->anonymousComponentPaths;
744+
}
745+
714746
/**
715747
* Get the registered anonymous component namespaces.
716748
*

src/Illuminate/View/Compilers/ComponentTagCompiler.php

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,58 @@ public function componentClass(string $component)
291291
return $class;
292292
}
293293

294-
$guess = collect($this->blade->getAnonymousComponentNamespaces())
294+
if (! is_null($guess = $this->guessAnonymousComponentUsingNamespaces($viewFactory, $component)) ||
295+
! is_null($guess = $this->guessAnonymousComponentUsingPaths($viewFactory, $component))) {
296+
return $guess;
297+
}
298+
299+
if (Str::startsWith($component, 'mail::')) {
300+
return $component;
301+
}
302+
303+
throw new InvalidArgumentException(
304+
"Unable to locate a class or view for component [{$component}]."
305+
);
306+
}
307+
308+
/**
309+
* Attempt to find an anonymous component using the registered anonymous component paths.
310+
*
311+
* @param \Illuminate\Contracts\View\Factory $viewFactory
312+
* @param string $component
313+
* @return string|null
314+
*/
315+
protected function guessAnonymousComponentUsingPaths(Factory $viewFactory, string $component)
316+
{
317+
if (str_contains($component, ViewFinderInterface::HINT_PATH_DELIMITER)) {
318+
return;
319+
}
320+
321+
foreach ($this->blade->getAnonymousComponentPaths() as $path) {
322+
try {
323+
if (! is_null($guess = match (true) {
324+
$viewFactory->exists($guess = md5($path).ViewFinderInterface::HINT_PATH_DELIMITER.$component) => $guess,
325+
$viewFactory->exists($guess = md5($path).ViewFinderInterface::HINT_PATH_DELIMITER.$component.'.index') => $guess,
326+
default => null,
327+
})) {
328+
return $guess;
329+
}
330+
} catch (InvalidArgumentException $e) {
331+
//
332+
}
333+
}
334+
}
335+
336+
/**
337+
* Attempt to find an anonymous component using the registered anonymous component namespaces.
338+
*
339+
* @param \Illuminate\Contracts\View\Factory $viewFactory
340+
* @param string $component
341+
* @return string|null
342+
*/
343+
protected function guessAnonymousComponentUsingNamespaces(Factory $viewFactory, string $component)
344+
{
345+
return collect($this->blade->getAnonymousComponentNamespaces())
295346
->filter(function ($directory, $prefix) use ($component) {
296347
return Str::startsWith($component, $prefix.'::');
297348
})
@@ -311,18 +362,6 @@ public function componentClass(string $component)
311362
return $view;
312363
}
313364
});
314-
315-
if (! is_null($guess)) {
316-
return $guess;
317-
}
318-
319-
if (Str::startsWith($component, 'mail::')) {
320-
return $component;
321-
}
322-
323-
throw new InvalidArgumentException(
324-
"Unable to locate a class or view for component [{$component}]."
325-
);
326365
}
327366

328367
/**

tests/View/Blade/BladeComponentTagCompilerTest.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,72 @@ public function testClasslessComponentsWithAnonymousComponentNamespaceWithIndexV
567567
'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
568568
}
569569

570+
public function testClasslessComponentsWithAnonymousComponentPath()
571+
{
572+
$container = new Container;
573+
574+
$container->instance(Application::class, $app = m::mock(Application::class));
575+
$container->instance(Factory::class, $factory = m::mock(Factory::class));
576+
577+
$app->shouldReceive('getNamespace')->once()->andReturn('App\\');
578+
579+
$factory->shouldReceive('exists')->andReturnUsing(function ($arg) {
580+
return $arg === md5('test-directory').'::panel.index';
581+
});
582+
583+
Container::setInstance($container);
584+
585+
$blade = m::mock(BladeCompiler::class)->makePartial();
586+
587+
$blade->shouldReceive('getAnonymousComponentPaths')->once()->andReturn([
588+
'test-directory',
589+
]);
590+
591+
$compiler = $this->compiler([], [], $blade);
592+
593+
$result = $compiler->compileTags('<x-panel />');
594+
595+
$this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\View\AnonymousComponent', 'panel', ['view' => '8ee975052836fdc7da2267cf8a580b80::panel.index','data' => []])
596+
<?php if (isset(\$attributes) && \$attributes instanceof Illuminate\View\ComponentAttributeBag && \$constructor = (new ReflectionClass(Illuminate\View\AnonymousComponent::class))->getConstructor()): ?>
597+
<?php \$attributes = \$attributes->except(collect(\$constructor->getParameters())->map->getName()->all()); ?>
598+
<?php endif; ?>
599+
<?php \$component->withAttributes([]); ?>\n".
600+
'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
601+
}
602+
603+
public function testClasslessIndexComponentsWithAnonymousComponentPath()
604+
{
605+
$container = new Container;
606+
607+
$container->instance(Application::class, $app = m::mock(Application::class));
608+
$container->instance(Factory::class, $factory = m::mock(Factory::class));
609+
610+
$app->shouldReceive('getNamespace')->once()->andReturn('App\\');
611+
612+
$factory->shouldReceive('exists')->andReturnUsing(function ($arg) {
613+
return $arg === md5('test-directory').'::panel';
614+
});
615+
616+
Container::setInstance($container);
617+
618+
$blade = m::mock(BladeCompiler::class)->makePartial();
619+
620+
$blade->shouldReceive('getAnonymousComponentPaths')->once()->andReturn([
621+
'test-directory',
622+
]);
623+
624+
$compiler = $this->compiler([], [], $blade);
625+
626+
$result = $compiler->compileTags('<x-panel />');
627+
628+
$this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\View\AnonymousComponent', 'panel', ['view' => '8ee975052836fdc7da2267cf8a580b80::panel','data' => []])
629+
<?php if (isset(\$attributes) && \$attributes instanceof Illuminate\View\ComponentAttributeBag && \$constructor = (new ReflectionClass(Illuminate\View\AnonymousComponent::class))->getConstructor()): ?>
630+
<?php \$attributes = \$attributes->except(collect(\$constructor->getParameters())->map->getName()->all()); ?>
631+
<?php endif; ?>
632+
<?php \$component->withAttributes([]); ?>\n".
633+
'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
634+
}
635+
570636
public function testAttributeSanitization()
571637
{
572638
$this->mockViewFactory();

0 commit comments

Comments
 (0)