如何在 Symfony2 中创建动态工厂
How to create a dynamic factory in Symfony2
在大多数情况下,我使用服务并利用服务容器。然而,我的一个 classes,我们称它为 MyClass,有一些依赖项是根据仅在运行时才知道的参数动态创建的。例如参数为'A',则应创建MyClass,依赖Connection、Logger、Dep1A、Dep2A、Dep3A。如果参数是B,应该是Connection,Logger,Dep1B,Dep2B,Dep3B。
我知道在 Symfony 中无法创建具有动态参数的服务,这些参数只有在构建容器后才知道。通过快速搜索,我发现 classic 解决方案是对这 3 个动态依赖项进行 setter 注入。问题是我需要在很多地方使用 MyClass,并且每次都创建这 3 个依赖项中的每一个是没有意义的。
我会说创建一个工厂:MyClassFactory 得到一个字符串参数和 return MyClass 相应地,应该是解决方案。然而,在 Symfony 中,事情变得复杂了,其中 Logger 和 Connection 是服务,我无法将它们自动注入工厂,因为同样,由于动态参数,我无法将工厂创建为服务。
如果您知道如何解决这个问题,我很乐意阅读。
Edit1: 我的情况的一个例子 - 我有一个大系统,我现在正在与搜索引擎集成。我的实体是,比方说,博客,Post,用户。我想在搜索引擎存储中索引它们,检查索引状态,搜索它们等等。在这个例子中,MyClass 实际上是 SearchIntegration
interface EntityConfigInterface {
public function getConfig();
}
class PostConfig implements EntityConfigInterface {
public function getConfig(){}
}
class UserConfig implements EntityConfigInterface {
public function getConfig(){}
}
interface EntityIndexInterface {
public function getRecordsToIndex();
public function countRecords();
}
class PostIndex implements EntityIndexInterface {
public function getRecordsToIndex(){}
public function countRecords(){}
}
class UserIndex implements EntityIndexInterface {
public function getRecordsToIndex(){}
public function countRecords(){}
}
class SearchIntegration {
public function __construct(Connection $connection, Logger $logger, $externalSearchEngine, EntityConfigInterface $config, EntityIndexInterface $index) {
}
public function checkStatusIndex($entityName) {
return $this->index->countRecords() === $this->externalSearchEngine->countRecords($entityName);
}
}
class SearchIntegrationFactory {
public static function build($entity) {
switch ($entity) {
case 'post':
return new SearchIntegration(new Connection, new Logger, new ExternalSearchEngine, new PostConfig, new PostIndex);
case 'user':
return new SearchIntegration(new Connection, new Logger, new ExternalSearchEngine, new UserConfig, new userIndex);
}
}
}
Class SearchIntegrationController {
public function checkIndexStatusForAllEntities() {
//HOW TO GET THE INSTANCE OF SearchIntegrationFactory::build('user')?
$userSearchIntegration = $this->get('search_integration');
$userStatus = $userSearchIntegration->checkStatusIndex();
//HOW TO GET THE INSTANCE OF SearchIntegrationFactory::build('post')?
$postSearchIntegration = $this->get('search_integration');
$postStatus = $postSearchIntegration->checkStatusIndex();
echo $userStatus, $postStatus;
}
}
Edit2:最终工厂 class 根据@John Noel 解决方案进行了一些调整:
Config.yml
search_integration_factory:
class: SearchIntegrationFactory
arguments:
[@service_container]
SearchIntegrationFactory Class:
class SearchIntegrationFactory {
private $container;
public function __construct(Container $container) {
$this->container = $container;
}
public function build($entity) {
$entityConfig = $this->container->get($entity . '_config');
$entityIndex = $this->container->get($entity . '_index');
return new SearchIntegration(
$this->container->get('Connection'),
$this->container->get('logger'),
new ExternalSearchEngine,
$entityConfig,
$entityIndex
);
}
}
然后构建class:
$this->get('search_integration_factory')->build('post');
$this->get('search_integration_factory')->build('user');
编辑: 所以根据你上面所说的,听起来你只需要将 SearchIntergrationFactory
设置为标准服务,然后在你的代码中抓住它并像往常一样调用您的 "build" 函数,而不是尝试从 DI 容器中调用它。例如
searchintegrationfactory:
class: SearchIntegrationFactory
calls:
- [ setLogger, [ @logger ] ]
然后在你的 SearchIntegrationController
:
// note, your factory shouldn't use a static method here
$userSearchIntegration = $this->get('searchintegrationfactory')->build($entity);
这与 Doctrine 进行实体管理的方式几乎相同,因为每次您需要一个存储库时,您都可以:
$this->get('doctrine')->getManager()->getRepository('Entity:Page');
有什么东西阻止你这样做吗?
--
听起来您可能想看看使用 Symfony 服务容器 (documentation) 的工厂功能。
根据你的描述,你的另一个 类 只想了解 MyClass
。在这种情况下,您的容器描述可能类似于:
myclass.factory:
class: MyClassFactory
arguments:
- @connection
- @logger
myclass:
class: MyClass # as per documentation, this isn't actually used
factory: [ @myclass.factory, "getMyClass" ]
arguments:
- %runtime_parameter% # if you can parameterise
- @service.to.get.runtime_parameter # if you can't
现在这不会处理您的其他依赖项(Dep1A
等),这取决于它们是什么,将指导您如何解决此问题。您可以将容器作为参数传递给您的工厂,并且只 get()
在 MyClass
创建期间需要的依赖项,或者您可以将您需要的所有依赖项传递给工厂并完成它。
两者都有其缺点,前者打破了服务描述的 "minimum surface area" 准则,而如果您有很多依赖项,后者可能站不住脚。
在不了解更多设置的情况下,这听起来像是某处可能需要重构或重新架构,以便不会出现这种情况,但这超出了解决此特定问题的范围。
在大多数情况下,我使用服务并利用服务容器。然而,我的一个 classes,我们称它为 MyClass,有一些依赖项是根据仅在运行时才知道的参数动态创建的。例如参数为'A',则应创建MyClass,依赖Connection、Logger、Dep1A、Dep2A、Dep3A。如果参数是B,应该是Connection,Logger,Dep1B,Dep2B,Dep3B。
我知道在 Symfony 中无法创建具有动态参数的服务,这些参数只有在构建容器后才知道。通过快速搜索,我发现 classic 解决方案是对这 3 个动态依赖项进行 setter 注入。问题是我需要在很多地方使用 MyClass,并且每次都创建这 3 个依赖项中的每一个是没有意义的。
我会说创建一个工厂:MyClassFactory 得到一个字符串参数和 return MyClass 相应地,应该是解决方案。然而,在 Symfony 中,事情变得复杂了,其中 Logger 和 Connection 是服务,我无法将它们自动注入工厂,因为同样,由于动态参数,我无法将工厂创建为服务。
如果您知道如何解决这个问题,我很乐意阅读。
Edit1: 我的情况的一个例子 - 我有一个大系统,我现在正在与搜索引擎集成。我的实体是,比方说,博客,Post,用户。我想在搜索引擎存储中索引它们,检查索引状态,搜索它们等等。在这个例子中,MyClass 实际上是 SearchIntegration
interface EntityConfigInterface {
public function getConfig();
}
class PostConfig implements EntityConfigInterface {
public function getConfig(){}
}
class UserConfig implements EntityConfigInterface {
public function getConfig(){}
}
interface EntityIndexInterface {
public function getRecordsToIndex();
public function countRecords();
}
class PostIndex implements EntityIndexInterface {
public function getRecordsToIndex(){}
public function countRecords(){}
}
class UserIndex implements EntityIndexInterface {
public function getRecordsToIndex(){}
public function countRecords(){}
}
class SearchIntegration {
public function __construct(Connection $connection, Logger $logger, $externalSearchEngine, EntityConfigInterface $config, EntityIndexInterface $index) {
}
public function checkStatusIndex($entityName) {
return $this->index->countRecords() === $this->externalSearchEngine->countRecords($entityName);
}
}
class SearchIntegrationFactory {
public static function build($entity) {
switch ($entity) {
case 'post':
return new SearchIntegration(new Connection, new Logger, new ExternalSearchEngine, new PostConfig, new PostIndex);
case 'user':
return new SearchIntegration(new Connection, new Logger, new ExternalSearchEngine, new UserConfig, new userIndex);
}
}
}
Class SearchIntegrationController {
public function checkIndexStatusForAllEntities() {
//HOW TO GET THE INSTANCE OF SearchIntegrationFactory::build('user')?
$userSearchIntegration = $this->get('search_integration');
$userStatus = $userSearchIntegration->checkStatusIndex();
//HOW TO GET THE INSTANCE OF SearchIntegrationFactory::build('post')?
$postSearchIntegration = $this->get('search_integration');
$postStatus = $postSearchIntegration->checkStatusIndex();
echo $userStatus, $postStatus;
}
}
Edit2:最终工厂 class 根据@John Noel 解决方案进行了一些调整:
Config.yml
search_integration_factory:
class: SearchIntegrationFactory
arguments:
[@service_container]
SearchIntegrationFactory Class:
class SearchIntegrationFactory {
private $container;
public function __construct(Container $container) {
$this->container = $container;
}
public function build($entity) {
$entityConfig = $this->container->get($entity . '_config');
$entityIndex = $this->container->get($entity . '_index');
return new SearchIntegration(
$this->container->get('Connection'),
$this->container->get('logger'),
new ExternalSearchEngine,
$entityConfig,
$entityIndex
);
}
}
然后构建class:
$this->get('search_integration_factory')->build('post');
$this->get('search_integration_factory')->build('user');
编辑: 所以根据你上面所说的,听起来你只需要将 SearchIntergrationFactory
设置为标准服务,然后在你的代码中抓住它并像往常一样调用您的 "build" 函数,而不是尝试从 DI 容器中调用它。例如
searchintegrationfactory:
class: SearchIntegrationFactory
calls:
- [ setLogger, [ @logger ] ]
然后在你的 SearchIntegrationController
:
// note, your factory shouldn't use a static method here
$userSearchIntegration = $this->get('searchintegrationfactory')->build($entity);
这与 Doctrine 进行实体管理的方式几乎相同,因为每次您需要一个存储库时,您都可以:
$this->get('doctrine')->getManager()->getRepository('Entity:Page');
有什么东西阻止你这样做吗?
--
听起来您可能想看看使用 Symfony 服务容器 (documentation) 的工厂功能。
根据你的描述,你的另一个 类 只想了解 MyClass
。在这种情况下,您的容器描述可能类似于:
myclass.factory:
class: MyClassFactory
arguments:
- @connection
- @logger
myclass:
class: MyClass # as per documentation, this isn't actually used
factory: [ @myclass.factory, "getMyClass" ]
arguments:
- %runtime_parameter% # if you can parameterise
- @service.to.get.runtime_parameter # if you can't
现在这不会处理您的其他依赖项(Dep1A
等),这取决于它们是什么,将指导您如何解决此问题。您可以将容器作为参数传递给您的工厂,并且只 get()
在 MyClass
创建期间需要的依赖项,或者您可以将您需要的所有依赖项传递给工厂并完成它。
两者都有其缺点,前者打破了服务描述的 "minimum surface area" 准则,而如果您有很多依赖项,后者可能站不住脚。
在不了解更多设置的情况下,这听起来像是某处可能需要重构或重新架构,以便不会出现这种情况,但这超出了解决此特定问题的范围。