如何在 Symfony 5 的其他目录中自动注册 'controllers as services'
How can I automatically register 'controllers as services' in other directories in Symfony 5
背景
我想使用 functional cohesion 来组织我的控制器。
这意味着我不会有一个 Controllers/
目录,这使得框架很容易,但我会按用例组织我的代码。任意示例:
src/FetchLatestNews/Controller.php
src/FetchLatestNews/News.php
src/FetchLatestNews/NewsRepository.php
- ...等等
但是,Symfony 默认设置为要求所有控制器都在一个地方。见 services.yaml
:
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
按照上面的模板,我 可以 将新的 src/FetchLatestNews/Controller
放在那里,然后每次都添加每个新控制器。但是,我不想,也不应该在每次创建新用例时手动更新此文件。
问题
如何避免此错误:
maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"
作为用户,我不必关心手动添加 service_arguments
或任何其他配置。当我指定一条路线时,它指向的东西因此是一个控制器。
理想的工作流程是:
- 在任何我喜欢的地方创建一个控制器。
- 添加一个路由到
routes.yaml
引用控制器。
- 就是这样!
是否可以在 Symfony 中实现此工作流程?应该是,因为路线中的任何东西 --> some class 都将成为控制器。有没有办法使用正则表达式或其他解决方案?
注意: 建议使用 'hack' 的空接口。这也不理想。还有其他选择吗?
Please note, this answer has been updated again with new information.
controller.service_arguments
标签只需要允许动作注入。坚持使用构造函数注入和 __invoke()
,这就是您所需要的。
下面的 public 添加是一项要求,但事实证明您不需要编译器通过,它可以在 services.yaml 级别完成。因此,您唯一需要的是:
services:
_defaults:
autowire: true
autoconfigure: true
public: true // <---- This is the new additional config.
然后,任何 class 都可以在 routes.yaml
中用作控制器,构造注入工作得很好。
Note: The original answer is below. Thanks to Jakumi for pointing me in the right direction.
您需要添加一个编译器传递来完成两件事。在启动和注册自动依赖注入的东西时,如果 class 名称中有 Controller
:
- 添加标签
controller.service_arguments
,这是您通常需要在 services.yaml
中手动添加的标签,以允许 setter 注入(如果您不这样做,可以忽略它)不关心 setter 注入)
- 将 class 设置为 public,因为 Symfony 有一个奇怪的想法,即不能从 class 访问中推断出 public 或私有任何东西的概念修饰符(认真的?)- 这是重要的事情,否则 Symfony 会抱怨控制器是私有的
别忘了您不必使用 "Controller in name" 的逻辑。它可以是 "Controller" 在 class 名称的末尾,这可能更好。
不管怎样,先创建一个CompilerPass
。把它放在任何你想要的地方。我把我的放在 Kernel.php
.
旁边
use Symfony\Component\DependencyInjection\{Compiler\CompilerPassInterface, ContainerBuilder};
class ControllersAsServices implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
foreach ($container->getDefinitions() as $definition) {
if (strpos($definition->getClass(), "Controller") === false) {
continue;
}
$definition->addTag("controller.service_arguments");
$definition->setPublic(true);
}
}
}
然后在你的 Kernel.php
中,你需要告诉它使用这个新的。覆盖受保护的函数 build
并添加您的编译器传递,如文档所示:
protected function build(ContainerBuilder $container)
{
$container->addCompilerPass(new ControllersAsServices, PassConfig::TYPE_BEFORE_OPTIMIZATION, -1);
parent::build($container);
}
清除您的开发缓存。在我这样做之前,我没有任何改变。
现在,名称中带有 Controller
的任何 class 都可以自动装配,因为它本来应该是这样的。
下面来自 Jakumi 的原始答案:
我相信在你的情况下最好的方法是实施 CompilerPass,将 classes 添加到容器中:
https://symfony.com/doc/current/bundles/extension.html#adding-classes-to-compile
在此过程中,可能还有一种方法可以添加标签。
Symfony 通过 RegisterControllerArgumentLocatorsPass 迎合 controller.service_arguments 标签,它解析构造函数参数并检查某些特征。如果你让你的编译器传递优先级更高,你可能会很容易地解决这个问题......
背景
我想使用 functional cohesion 来组织我的控制器。
这意味着我不会有一个 Controllers/
目录,这使得框架很容易,但我会按用例组织我的代码。任意示例:
src/FetchLatestNews/Controller.php
src/FetchLatestNews/News.php
src/FetchLatestNews/NewsRepository.php
- ...等等
但是,Symfony 默认设置为要求所有控制器都在一个地方。见 services.yaml
:
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
按照上面的模板,我 可以 将新的 src/FetchLatestNews/Controller
放在那里,然后每次都添加每个新控制器。但是,我不想,也不应该在每次创建新用例时手动更新此文件。
问题
如何避免此错误:
maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"
作为用户,我不必关心手动添加 service_arguments
或任何其他配置。当我指定一条路线时,它指向的东西因此是一个控制器。
理想的工作流程是:
- 在任何我喜欢的地方创建一个控制器。
- 添加一个路由到
routes.yaml
引用控制器。 - 就是这样!
是否可以在 Symfony 中实现此工作流程?应该是,因为路线中的任何东西 --> some class 都将成为控制器。有没有办法使用正则表达式或其他解决方案?
注意:
Please note, this answer has been updated again with new information.
controller.service_arguments
标签只需要允许动作注入。坚持使用构造函数注入和 __invoke()
,这就是您所需要的。
下面的 public 添加是一项要求,但事实证明您不需要编译器通过,它可以在 services.yaml 级别完成。因此,您唯一需要的是:
services:
_defaults:
autowire: true
autoconfigure: true
public: true // <---- This is the new additional config.
然后,任何 class 都可以在 routes.yaml
中用作控制器,构造注入工作得很好。
Note: The original answer is below. Thanks to Jakumi for pointing me in the right direction.
您需要添加一个编译器传递来完成两件事。在启动和注册自动依赖注入的东西时,如果 class 名称中有 Controller
:
- 添加标签
controller.service_arguments
,这是您通常需要在services.yaml
中手动添加的标签,以允许 setter 注入(如果您不这样做,可以忽略它)不关心 setter 注入) - 将 class 设置为 public,因为 Symfony 有一个奇怪的想法,即不能从 class 访问中推断出 public 或私有任何东西的概念修饰符(认真的?)- 这是重要的事情,否则 Symfony 会抱怨控制器是私有的
别忘了您不必使用 "Controller in name" 的逻辑。它可以是 "Controller" 在 class 名称的末尾,这可能更好。
不管怎样,先创建一个CompilerPass
。把它放在任何你想要的地方。我把我的放在 Kernel.php
.
use Symfony\Component\DependencyInjection\{Compiler\CompilerPassInterface, ContainerBuilder};
class ControllersAsServices implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
foreach ($container->getDefinitions() as $definition) {
if (strpos($definition->getClass(), "Controller") === false) {
continue;
}
$definition->addTag("controller.service_arguments");
$definition->setPublic(true);
}
}
}
然后在你的 Kernel.php
中,你需要告诉它使用这个新的。覆盖受保护的函数 build
并添加您的编译器传递,如文档所示:
protected function build(ContainerBuilder $container)
{
$container->addCompilerPass(new ControllersAsServices, PassConfig::TYPE_BEFORE_OPTIMIZATION, -1);
parent::build($container);
}
清除您的开发缓存。在我这样做之前,我没有任何改变。
现在,名称中带有 Controller
的任何 class 都可以自动装配,因为它本来应该是这样的。
下面来自 Jakumi 的原始答案:
我相信在你的情况下最好的方法是实施 CompilerPass,将 classes 添加到容器中:
https://symfony.com/doc/current/bundles/extension.html#adding-classes-to-compile
在此过程中,可能还有一种方法可以添加标签。
Symfony 通过 RegisterControllerArgumentLocatorsPass 迎合 controller.service_arguments 标签,它解析构造函数参数并检查某些特征。如果你让你的编译器传递优先级更高,你可能会很容易地解决这个问题......