如何在 Laravel 中添加额外的视图组件 class 路径?

How can I add extra view component class paths in Laravel?

问题

视图组件的默认命名空间是 App\View\Components,文件夹是 app/View/Components。我正在设置 DDD 文件结构并希望做两件事:

  1. 将“共享”视图组件分别移动到 App\ViewComponentssrc/app/ViewComponents 的命名空间和文件夹中
  2. 具有特定于各个“应用程序”的视图组件,它们分别具有自己的 App\MyApplication\ViewComponents/src/app/MyApplication/ViewComponets 命名空间和文件夹。

新的 App namespace/folder 设置是通过 composer psr-4 自动加载密钥完成的,并且工作正常。但是 Laravel 在尝试加载组件时总是使用 App\View\Components 命名空间。

我的尝试

我已经解决了问题的第一部分,但我希望有更好的方法。例如,当我想移动视图时,我可以只在我的 AppServiceProvider 中设置 view.paths 配置指令,但我没有看到类似的方式,本质上,将名称空间添加到 Laravel 的位置寻找视图组件。所以我最后做的是:

public function registerBladeEngine($resolver)
{
    // The Compiler engine requires an instance of the CompilerInterface, which in
    // this case will be the Blade compiler, so we'll first create the compiler
    // instance to pass into the engine so it can compile the views properly.
    $this->app->singleton('blade.compiler', function () {
        return new BladeCompiler(
            $this->app['files'],
            $this->app['config']['view.compiled'],
        );
    });

    $resolver->register('blade', function () {
        return new CompilerEngine(
            $this->app['blade.compiler']
        );
    });
}

如您所见,仅更改路径就需要大量工作。而且我还想添加 另一个 路径。多个“应用程序”运行 在同一个 Laravel 代码库下,因此例如我们可能有 App\Website\App\AdminApp\Blog,这取决于当前是哪个应用程序运行ning,为 运行ning 应用加载不同的命名空间,即博客将 App\Blog\ViewComponents 指向 src/app/Blog/ViewComponents

有没有一种方法可以实现这一点,而无需像上面那样重写?如果没有,你能建议一种方法来实现要求的第二部分吗?

注意:我还没有排除使用子文件夹并继续主 App\View\Components 命名空间下的所有内容的可能性 - 我不想打架 Laravel 如果没有更好的方法,我也愿意承认,但如果我能实现我想要的文件夹结构,它会感觉更整洁。

更新;通过使用配置和 php 8 个注释

获得了一个有效的实现

根据您的问题和提供的详细信息,按照以下步骤可以为 blade 查看组件功能添加更多查找文件夹。发布您已有的代码会有所帮助。但是我已经添加了一个可能的解决方案来让它工作,使用注释并使用带有 namespace/path 映射的配置。

根据您在一个应用程序和另一个应用程序之间切换的方式,您的问题中未提供详细信息,您必须修改 MyComponentTagCompiler class 中检索配置的方式.

Blade编译器

为了改变 ComponentTagCompiler 我们需要改变 BladeCompiler class:

namespace App;

class YourBladeCompiler extends \Illuminate\View\Compilers\BladeCompiler
{
    protected function compileComponentTags($value)
    {
        if (! $this->compilesComponentTags) {
            return $value;
        }

        return (new \App\MyComponentTagCompiler( //it is about this line 
            $this->classComponentAliases, $this->classComponentNamespaces, $this
        ))->compile($value);
    }
}

服务提供商

现在在YourViewServiceProvider 中注册YourBladeCompiler:

class YourViewServiceProvider extends \Illuminate\View\ViewServiceProvider
{
    public function registerBladeEngine($resolver)
    {
        $this->app->singleton('blade.compiler', function () {
            return new \App\YourBladeCompiler( //it is about this line 
                $this->app['files'],
                $this->app['config']['view.compiled'],
            );
        });

        $resolver->register('blade', function () {
            return new CompilerEngine(
                $this->app['blade.compiler']
            );
        });
    }
}

MyComponentTagCompiler

这是我创建的与 PHP 8 个属性一起使用的实现,如下所示:

namespace App;

#[\Attribute]
class ViewComponentName
{
    public string $name;
    public string $package;

    public function __construct(string $name, string $package)
    {
        $this->name = $name;
        $this->package = $package;
    }
}

通过该属性,您可以在视图组件上声明包名和组件名class(见底部示例)。因此,在查找过程中,可以根据这些参数匹配组件。

但如果需要,您可以将其更改为您自己的要求。

它的作用:

  • 首先让我们Laravel在parent::componentClass方法中通过它自己的机制查找视图组件。
  • 如果未找到组件并抛出异常 (InvalidArgumentException),之后我的实现将遍历给定的路径和名称空间(来自 getLookupPaths 方法)并查看是否有属性匹配组件名称和包名称。如果是这样,它 returns 这个 class 和视图组件相应地加载。
namespace App;

use App\View\ViewComponentName;
use Illuminate\View\Compilers\ComponentTagCompiler;

class MyComponentTagCompiler extends ComponentTagCompiler
{
    protected function getLookupPaths() : array
    {
        /*
         * add some logic here to get an application specific configuration
         * since you have multiple application in one, I cannot know it works in your
         * application, since the details are not provided in the question 
         */
        return config('view_component_paths');
    }

    private function getFiles(string $dir) : array
    {
        return scandir($dir);
    }

    private function isPhpFile(string $file) : bool
    {
        return strpos($file, ".php");
    }

    private function getClassNamespace(string $file, string $folderNamespace) : string
    {
        $class = str_replace(".php", "", $file);
        $classNamespace = $folderNamespace . "\" . $class;
        return $classNamespace;
    }

    private function getComponentName(string $file, string $namespace) : ?ViewComponentName
    {
        $classNamespace = $this->getClassNamespace($file, $namespace);
        $reflection = new \ReflectionClass($classNamespace);
        if(method_exists($reflection, 'getAttributes')) {
            $attribute = $reflection->getAttributes()[0];
            if ($attribute->getName() == ViewComponentName::class) {
                return $attribute->newInstance();
            }
        }
        return null;
    }

    public function componentClass(string $component)
    {
        try {
            parent::componentClass($component);
        } catch(\InvalidArgumentException $e) {
            list($lookupComponentPackage, $lookupComponentName) = explode("-", $component);
            foreach($this->getLookupPaths() as $namespace=>$dir) {
                foreach ($this->getFiles($dir) as $file) {
                    if ($this->isPhpFile($file)) {
                        if($componentName = $this->getComponentName($file, $namespace)) {
                            if($componentName->name == $lookupComponentName && $componentName->package == $lookupComponentPackage) {
                                return $this->getClassNamespace($file, $namespace);
                            }
                        }
                    }
                }
            }
            throw $e;
        }
    }
}

其中配置包含 (config/view_component_paths.php):

return [
    "App\Test"=>__DIR__ . "/Test/"
];

如果您希望完全替换默认的 laravel 行为或不喜欢我基于注释的实现,请考虑实现您自己的方法版本:

public function componentClass(string $component)
{
    //return the class name here based the component name
    //without calling parent
    dd($component); 
}

示例视图组件

namespace App\Test;
use App\View\ViewComponentName;
use Illuminate\View\Component;

#[ViewComponentName('test', 'namespace')]
class MyViewComponent extends Component
{
    public function render()
    {
        return view('components.test');
    }
}

在blade中:

<x-namespace-test />

它现在应该可以工作了。我认为这些信息足以让您了解如何在您自己的应用程序中实现它。似乎除了扩展一些基础 classes 之外别无他法。但是看看这个答案,可以创建一个基于全局查找配置和 php 注释的高级实现(或者你希望的一些其他机制,例如将带有命名空间的 class 名称转换为查看组件名称)。

旧答案

您问题中定义的问题 2

Have view components specific to individual "apps" with their own namespace and folder of App\MyApplication\ViewComponents and /src/app/MyApplication/ViewComponets respectively.

遗憾的是,似乎无法为 Laravel 中的视图组件定义多个 class 路径。但是您可以更改应用程序路径和命名空间前缀。据我所知,您只需覆盖 Application class.

中的以下属性

bootstrap/app.php

替换以下行:

$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

与:

class YourApplication extends \Illuminate\Foundation\Application
{
    protected $namespace = "App\MyApplication";
    protected $appPath = __DIR__ . "/../app/MyApplication";
}

$app = new YourApplication(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

这足以将应用程序文件夹更改为另一个文件夹,并让您了解如何动态更改它以在不同命名空间中拥有多个应用程序。如果您现在 运行 laravel 命令如 php artisan make:component Test1234 它会在您的新应用程序文件夹中创建:app/MyApplication/View/Components/Test1234.php.

硬编码路径

View/Components这样的一些路径在Laravel中是硬编码的,因此不容易改变。如果您按照上面的定义进行更改,在这种情况下,视图组件命名空间变为:App\MyApplication\View\Components 和路径:app/MyApplication/View/Components.

您问题中定义的问题 1

Move "shared" view components to a namespace and folder of App\ViewComponents and src/app/ViewComponents respectively

当您如上所述更改应用程序路径时,不可能有一个“共享的”View Component 文件夹。 Laravel,看起来只有一个默认的 View Components 路径,它基于硬编码路径和动态命名空间前缀,如上所述。但是您当然可以创建共享命名空间并手动注册视图组件:

查看组件(app/ViewComponents/文件夹)

namespace App\ViewComponents;

use Illuminate\View\Component;

class Test extends Component
{
    public function render()
    {
        return view('components.test');
    }
}

不要忘记 components.test blade 视图。

服务提供商

\Blade::component("shared-test",\App\ViewComponents\Test::class);

Blade

<x-shared-test />