Skip to content

Commit

Permalink
Anonymous component paths (#45338)
Browse files Browse the repository at this point in the history
* allow registration of other anonymous component paths

* add test

* check for delimiter and bail early if possible

* fix test

* add index test
  • Loading branch information
taylorotwell authored Dec 16, 2022
1 parent 684a512 commit 5a1eb8b
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 13 deletions.
32 changes: 32 additions & 0 deletions src/Illuminate/View/Compilers/BladeCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ class BladeCompiler extends Compiler implements CompilerInterface
*/
protected $rawBlocks = [];

/**
* The array of anonymous component paths to search for components in.
*
* @var array
*/
protected $anonymousComponentPaths = [];

/**
* The array of anonymous component namespaces to autoload from.
*
Expand Down Expand Up @@ -682,6 +689,21 @@ public function getClassComponentAliases()
return $this->classComponentAliases;
}

/**
* Register a new anonymous component path.
*
* @param string $path
* @return void
*/
public function anonymousComponentPath(string $path)
{
$this->anonymousComponentPaths[] = $path;

Container::getInstance()
->make(ViewFactory::class)
->addNamespace(md5($path), $path);
}

/**
* Register an anonymous component namespace.
*
Expand Down Expand Up @@ -711,6 +733,16 @@ public function componentNamespace($namespace, $prefix)
$this->classComponentNamespaces[$prefix] = $namespace;
}

/**
* Get the registered anonymous component paths.
*
* @return array
*/
public function getAnonymousComponentPaths()
{
return $this->anonymousComponentPaths;
}

/**
* Get the registered anonymous component namespaces.
*
Expand Down
65 changes: 52 additions & 13 deletions src/Illuminate/View/Compilers/ComponentTagCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,58 @@ public function componentClass(string $component)
return $class;
}

$guess = collect($this->blade->getAnonymousComponentNamespaces())
if (! is_null($guess = $this->guessAnonymousComponentUsingNamespaces($viewFactory, $component)) ||
! is_null($guess = $this->guessAnonymousComponentUsingPaths($viewFactory, $component))) {
return $guess;
}

if (Str::startsWith($component, 'mail::')) {
return $component;
}

throw new InvalidArgumentException(
"Unable to locate a class or view for component [{$component}]."
);
}

/**
* Attempt to find an anonymous component using the registered anonymous component paths.
*
* @param \Illuminate\Contracts\View\Factory $viewFactory
* @param string $component
* @return string|null
*/
protected function guessAnonymousComponentUsingPaths(Factory $viewFactory, string $component)
{
if (str_contains($component, ViewFinderInterface::HINT_PATH_DELIMITER)) {
return;
}

foreach ($this->blade->getAnonymousComponentPaths() as $path) {
try {
if (! is_null($guess = match (true) {
$viewFactory->exists($guess = md5($path).ViewFinderInterface::HINT_PATH_DELIMITER.$component) => $guess,
$viewFactory->exists($guess = md5($path).ViewFinderInterface::HINT_PATH_DELIMITER.$component.'.index') => $guess,
default => null,
})) {
return $guess;
}
} catch (InvalidArgumentException $e) {
//
}
}
}

/**
* Attempt to find an anonymous component using the registered anonymous component namespaces.
*
* @param \Illuminate\Contracts\View\Factory $viewFactory
* @param string $component
* @return string|null
*/
protected function guessAnonymousComponentUsingNamespaces(Factory $viewFactory, string $component)
{
return collect($this->blade->getAnonymousComponentNamespaces())
->filter(function ($directory, $prefix) use ($component) {
return Str::startsWith($component, $prefix.'::');
})
Expand All @@ -311,18 +362,6 @@ public function componentClass(string $component)
return $view;
}
});

if (! is_null($guess)) {
return $guess;
}

if (Str::startsWith($component, 'mail::')) {
return $component;
}

throw new InvalidArgumentException(
"Unable to locate a class or view for component [{$component}]."
);
}

/**
Expand Down
66 changes: 66 additions & 0 deletions tests/View/Blade/BladeComponentTagCompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,72 @@ public function testClasslessComponentsWithAnonymousComponentNamespaceWithIndexV
'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
}

public function testClasslessComponentsWithAnonymousComponentPath()
{
$container = new Container;

$container->instance(Application::class, $app = m::mock(Application::class));
$container->instance(Factory::class, $factory = m::mock(Factory::class));

$app->shouldReceive('getNamespace')->once()->andReturn('App\\');

$factory->shouldReceive('exists')->andReturnUsing(function ($arg) {
return $arg === md5('test-directory').'::panel.index';
});

Container::setInstance($container);

$blade = m::mock(BladeCompiler::class)->makePartial();

$blade->shouldReceive('getAnonymousComponentPaths')->once()->andReturn([
'test-directory',
]);

$compiler = $this->compiler([], [], $blade);

$result = $compiler->compileTags('<x-panel />');

$this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\View\AnonymousComponent', 'panel', ['view' => '8ee975052836fdc7da2267cf8a580b80::panel.index','data' => []])
<?php if (isset(\$attributes) && \$attributes instanceof Illuminate\View\ComponentAttributeBag && \$constructor = (new ReflectionClass(Illuminate\View\AnonymousComponent::class))->getConstructor()): ?>
<?php \$attributes = \$attributes->except(collect(\$constructor->getParameters())->map->getName()->all()); ?>
<?php endif; ?>
<?php \$component->withAttributes([]); ?>\n".
'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
}

public function testClasslessIndexComponentsWithAnonymousComponentPath()
{
$container = new Container;

$container->instance(Application::class, $app = m::mock(Application::class));
$container->instance(Factory::class, $factory = m::mock(Factory::class));

$app->shouldReceive('getNamespace')->once()->andReturn('App\\');

$factory->shouldReceive('exists')->andReturnUsing(function ($arg) {
return $arg === md5('test-directory').'::panel';
});

Container::setInstance($container);

$blade = m::mock(BladeCompiler::class)->makePartial();

$blade->shouldReceive('getAnonymousComponentPaths')->once()->andReturn([
'test-directory',
]);

$compiler = $this->compiler([], [], $blade);

$result = $compiler->compileTags('<x-panel />');

$this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\View\AnonymousComponent', 'panel', ['view' => '8ee975052836fdc7da2267cf8a580b80::panel','data' => []])
<?php if (isset(\$attributes) && \$attributes instanceof Illuminate\View\ComponentAttributeBag && \$constructor = (new ReflectionClass(Illuminate\View\AnonymousComponent::class))->getConstructor()): ?>
<?php \$attributes = \$attributes->except(collect(\$constructor->getParameters())->map->getName()->all()); ?>
<?php endif; ?>
<?php \$component->withAttributes([]); ?>\n".
'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
}

public function testAttributeSanitization()
{
$this->mockViewFactory();
Expand Down

0 comments on commit 5a1eb8b

Please sign in to comment.