From 5a1eb8b2df7db5e4b456f0efb9bb574dd3edeaf8 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 16 Dec 2022 10:59:37 -0600 Subject: [PATCH] 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 --- .../View/Compilers/BladeCompiler.php | 32 +++++++++ .../View/Compilers/ComponentTagCompiler.php | 65 ++++++++++++++---- .../Blade/BladeComponentTagCompilerTest.php | 66 +++++++++++++++++++ 3 files changed, 150 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php index 51b5abd69c5a..34cd67e341aa 100644 --- a/src/Illuminate/View/Compilers/BladeCompiler.php +++ b/src/Illuminate/View/Compilers/BladeCompiler.php @@ -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. * @@ -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. * @@ -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. * diff --git a/src/Illuminate/View/Compilers/ComponentTagCompiler.php b/src/Illuminate/View/Compilers/ComponentTagCompiler.php index 4ae953b3461b..2d109716ac65 100644 --- a/src/Illuminate/View/Compilers/ComponentTagCompiler.php +++ b/src/Illuminate/View/Compilers/ComponentTagCompiler.php @@ -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.'::'); }) @@ -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}]." - ); } /** diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php index 0fdd7295d8ac..695f6f0934da 100644 --- a/tests/View/Blade/BladeComponentTagCompilerTest.php +++ b/tests/View/Blade/BladeComponentTagCompilerTest.php @@ -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(''); + + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\View\AnonymousComponent', 'panel', ['view' => '8ee975052836fdc7da2267cf8a580b80::panel.index','data' => []]) +getConstructor()): ?> +except(collect(\$constructor->getParameters())->map->getName()->all()); ?> + +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(''); + + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\View\AnonymousComponent', 'panel', ['view' => '8ee975052836fdc7da2267cf8a580b80::panel','data' => []]) +getConstructor()): ?> +except(collect(\$constructor->getParameters())->map->getName()->all()); ?> + +withAttributes([]); ?>\n". + '@endComponentClass##END-COMPONENT-CLASS##', trim($result)); + } + public function testAttributeSanitization() { $this->mockViewFactory();