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

查看:11
本文介绍了如何在 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 的硬依赖,一旦 bundle 被禁用,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

只要在运行时实际替换接口,就可以工作.因为 bundle 依赖应该是可选的,所以很可能没有可用的具体实现(即没有加载 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 来说,让用户成为拥有方最有意义,只要只涉及两个包,让 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.

使用单表继承

只要可选包是唯一与新添加的关联交互的包,为每个包提供自己的扩展 UserBundleEntitiesUser 的 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 UserBundleEntitiesUser 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 NameMyBundleCore;

use DoctrineORMEventLoadClassMetadataEventArgs;
use DoctrineORMMappingClassMetadata;

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: NameMyBundleCoreResolveTargetEntityListener

这会将原始 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 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天全站免登陆