Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anonymous component paths #45338

Merged
merged 5 commits into from
Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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