如何在Symfony 2中进行可选的跨捆绑关联? [英] How to do optional cross-bundle associations in Symfony 2?

查看:56
本文介绍了如何在Symfony 2中进行可选的跨捆绑关联?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在研究一个使用Doctrine 2 ORM的Symfony 2.3项目。不出所料,功能被拆分并分成了大部分独立的捆绑包,以允许在其他项目中重复使用代码。

I'm working on a Symfony 2.3 Project that utilizes the Doctrine 2 ORM. As is to be expected functionality is split and grouped into mostly independent bundles to allow for code-reuse in other projects.

我有一个UserBundle和一个ContactInfoBundle。因为其他实体可能具有关联的联系信息,所以联系信息被拆分,但是在用户不需要所述联系信息的情​​况下构建系统并不是不可思议的。因此,我非常希望这两个不共享任何硬链接。

I have a UserBundle and a ContactInfoBundle. The contact info is split off because other entities could have contact information associated, however it is not inconcievable that a system may be built where users do not require said contact information. As such I'd very much prefer these two do not share any hard links.

但是,创建从User实体到ContactInfo实体的关联映射会创建硬依赖关系在ContactInfoBundle上,禁用捆绑后,Doctrine会立即抛出错误,表明ContactInfo不在其任何已注册的命名空间中。

However, creating the association mapping from the User entity to the ContactInfo entity creates a hard dependency on the ContactInfoBundle, as soon as the bundle is disabled Doctrine throws errors that ContactInfo is not within any of its registered namespaces.

我的调查发现了一些应该可以解决这个问题,但是它们似乎都不能完全发挥作用:

My investigations have uncovered several strategies that are supposed to counter this, but none of them seem fully functional:


  1. Doctrine 2的ResolveTargetEntityListener

此方法有效,只要接口实际上是在运行时替换。因为捆绑依赖应该是可选的,所以很可能没有具体的实现(即没有加载contactInfoBundle)

This works, as long as the interface is actually replaced at runtime. Because the bundle dependency is supposed to be optional, it could very well be that there is NO concrete implementation available (i.e. contactInfoBundle is not loaded)

如果没有目标实体,由于占位符对象不是实体(并且不在/ Entity命名空间之内),因此整个配置崩溃了,理论上可以将它们链接到实际上没有做任何事情的Mock实体。但是此实体随后获得了自己的表(并对其进行查询),从而打开了全新的蠕虫罐。

If there is no target entity, the entire configuration collapses onto itself because the placeholder object is not an entity (and is not within the /Entity namespace), one could theoretically link them to a Mock entity that doesn't really do anything. But this entity then gets its own table (and it gets queried), opening up a whole new can of worms.

逆关系

对于ContactInfo而言,让User成为拥有方最有意义,使ContactInfo成为拥有方成功避开了依赖项的可选部分,只要涉及两个捆绑。但是,一旦第三个(也是可选的)捆绑软件希望与ContactInfo建立(可选的)链接,则使ContactInfo成为拥有方会在第三个捆绑软件上从ContactInfo产生硬依赖性。

For the ContactInfo it makes the most sense for User to be the owning side, making ContactInfo the owning side successfully sidesteps the optional part of the dependency as long as only two bundles are involved. However, as soon as a third (also optional) bundle desires an (optional) link with ContactInfo, making ContactInfo the owning side creates a hard dependency from ContactInfo on the third bundle.

使用户拥有逻辑逻辑是一种特殊情况。但是,问题普遍存在,实体A包含B,C包含B。

Making User the owning side being logical is a specific situation. The issue however is universal where entity A contains B, and C contains B.

使用单表继承

只要可选捆绑包是与新添加的关联进行交互的唯一捆绑包,则为每个捆绑包分配扩展UserBundle\Entities\User的自己的User实体即可。但是,具有多个扩展单个实体的捆绑包会使它变得一团糟。您永远无法完全确定哪些功能在哪里可用,以及让控制器以某种方式响应打开和/或关闭的捆绑包(由Symfony 2的DependencyInjection机制支持)几乎是不可能的。

As long as the optional bundles are the only one that interacts with the newly added association, giving each bundle their own User entity that extends UserBundle\Entities\User could work. However having multiple bundles that extend a single entity rapidly causes this to become a bit of a mess. You can never be completely sure what functions are available where, and having controllers somehow respond to bundles being on and/or off (as is supported by Symfony 2's DependencyInjection mechanics) becomes largely impossible.

欢迎提供任何有关如何解决此问题的想法或见解。在闯入砖墙几天后,我有了新的想法。有人会期望Symfony有某种方法可以做到这一点,但是文档只提供了次优的ResolveTargetEntityListener。

Any ideas or insights in how to circumvent this problem are welcome. After a couple of days of running into brick walls I'm fresh out of ideas. One would expect Symfony to have some method of doing this, but the documentation only comes up with the ResolveTargetEntityListener, which is sub-optimal.

推荐答案

我终于设法为该问题找到了适合我项目的解决方案。作为介绍,我应该说我的体系结构中的束是星状布置的。我的意思是说我有一个核心或基本捆绑包,它充当基础依赖模块,并存在于所有项目中。所有其他捆绑包都可以依靠它,并且只能依靠它。我的其他捆绑包之间没有直接依赖关系。我非常确定,由于体系结构简单,因此该提议的解决方案在这种情况下可以使用。我还应该说,我担心此方法可能会涉及调试问题,但可以这样做,以便根据配置设置轻松打开或关闭它。

I have finally managed to rig up a solution to this problem which would be suited for my project. As an introduction, I should say that the bundles in my architecture are laid out "star-like". By that I mean that I have one core or base bundle which serves as the base dependency module and is present in all the projects. All other bundles can rely on it and only it. There are no direct dependencies between my other bundles. I'm quite certain that this proposed solution would work in this case because of the simplicity in the architecture. I should also say that I fear there could be debugging issues involved with this method, but it could be made so that it is easily switched on or off, depending on a configuration setting, for instance.

基本思想是建立我自己的ResolveTargetEntityListener,如果缺少相关实体,它将跳过相关实体。如果缺少绑定到接口的类,这将允许执行过程继续。可能无需强调拼写错误在配置中的含义-找不到类,这会产生难以调试的错误。因此,我建议在开发阶段将其关闭,然后在生产中将其重新打开。这样,所有可能的错误都将由教义指出。

The basic idea is to rig up my own ResolveTargetEntityListener, which would skip relating the entities if the related entity is missing. This would allow the process of execution to continue if there is a class bound to the interface missing. There's probably no need to emphasize the implication of the typo in the configuration - the class won't be found and this can produce a hard-to-debug error. That's why I'd advise to turn it off during the development phase and then turn it back on in the production. This way, all the possible errors will be pointed out by the Doctrine.

重用ResolveTargetEntityListener的代码,并将一些其他代码放入 remapAssociation 方法中。这是我的最终实现:

The implementation consists of reusing the ResolveTargetEntityListener's code and putting some additional code inside the remapAssociation method. This is my final implementation:

<?php
namespace Name\MyBundle\Core;

use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Mapping\ClassMetadata;

class ResolveTargetEntityListener
{
    /**
     * @var array
     */
    private $resolveTargetEntities = array();

    /**
     * Add a target-entity class name to resolve to a new class name.
     *
     * @param string $originalEntity
     * @param string $newEntity
     * @param array $mapping
     * @return void
     */
    public function addResolveTargetEntity($originalEntity, $newEntity, array $mapping)
    {
        $mapping['targetEntity'] = ltrim($newEntity, "\\");
        $this->resolveTargetEntities[ltrim($originalEntity, "\\")] = $mapping;
    }

    /**
     * Process event and resolve new target entity names.
     *
     * @param LoadClassMetadataEventArgs $args
     * @return void
     */
    public function loadClassMetadata(LoadClassMetadataEventArgs $args)
    {
        $cm = $args->getClassMetadata();
        foreach ($cm->associationMappings as $mapping) {
            if (isset($this->resolveTargetEntities[$mapping['targetEntity']])) {
                $this->remapAssociation($cm, $mapping);
            }
        }
    }

    private function remapAssociation($classMetadata, $mapping)
    {
        $newMapping = $this->resolveTargetEntities[$mapping['targetEntity']];
        $newMapping = array_replace_recursive($mapping, $newMapping);
        $newMapping['fieldName'] = $mapping['fieldName'];

        unset($classMetadata->associationMappings[$mapping['fieldName']]);

        // Silently skip mapping the association if the related entity is missing
        if (class_exists($newMapping['targetEntity']) === false)
        {
            return;
        }

        switch ($mapping['type'])
        {
            case ClassMetadata::MANY_TO_MANY:
                $classMetadata->mapManyToMany($newMapping);
                break;
            case ClassMetadata::MANY_TO_ONE:
                $classMetadata->mapManyToOne($newMapping);
                break;
            case ClassMetadata::ONE_TO_MANY:
                $classMetadata->mapOneToMany($newMapping);
                break;
            case ClassMetadata::ONE_TO_ONE:
                $classMetadata->mapOneToOne($newMapping);
                break;
        }
    }
}

请注意在 switch 语句,用于映射实体关系。如果相关实体的类不存在,则该方法仅返回,而不执行错误的映射并产生错误。这也意味着缺少字段(如果不是多对多关系)。在这种情况下,外键只会在数据库中丢失,但是由于它存在于实体类中,因此所有代码仍然有效(如果不小心调用外键的getter或setter方法,则不会丢失方法错误)。

Note the silent return before the switch statement which is used to map the entity relations. If the related entity's class does not exist, the method just returns, rather than executing faulty mapping and producing the error. This also has the implication of a field missing (if it's not a many-to-many relation). The foreign key in that case will just be missing inside the database, but as it exists in the entity class, all the code is still valid (you won't get a missing method error if accidentally calling the foreign key's getter or setter).

要使用此代码,您只需更改一个参数即可。您应该将此更新的参数放在将始终加载的服务文件或其他类似位置。目标是将其放置在始终使用的地方,无论您要使用什么捆。我已将其放在我的基本捆绑包服务文件中:

To be able to use this code, you just have to change one parameter. You should put this updated parameter to a services file which will always be loaded or some other similar place. The goal is to have it at a place that will always be used, no matter what bundles you are going to use. I've put it in my base bundle services file:

doctrine.orm.listeners.resolve_target_entity.class: Name\MyBundle\Core\ResolveTargetEntityListener

这会将原始ResolveTargetEntityListener重定向到您的版本。为了以防万一,您还应该清除并预热缓存,以防万一。

This will redirect the original ResolveTargetEntityListener to your version. You should also clear and warm your cache after putting it in place, just in case.

I仅做过几次简单的测试,这些测试证明该方法可以按预期工作。我打算在接下来的几周内经常使用这种方法,如果需要的话,我将对其进行跟进。我也希望从其他决定尝试的人那里得到一些有用的反馈。

I have done only a couple of simple tests which have proven that this approach might work as expected. I intend to use this method frequently in the next couple of weeks and will be following up on it if the need arises. I also hope to get some useful feedback from other people who decide to give it a go.

这篇关于如何在Symfony 2中进行可选的跨捆绑关联?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆