如何在Symfony 2中执行可选的交叉绑定关联? [英] How do do optional cross-bundle associations in Symfony 2?

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

问题描述

我正在研究利用教义2 ORM的Symfony 2.3项目。正如预期的那样,功能被拆分并分组成大部分独立的软件包,以允许其他项目中的代码重用。



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



然而,创建从User实体到ContactInfo实体的关联映射创建了一个硬依赖在ContactInfoBundle上,只要捆绑包被禁用,Doctrine会引发ContactInfo不在其注册名称空间内的错误。



我的调查发现了几个策略,反对这一点,但没有一个似乎完全发挥作用:


  1. Doctrine 2的ResolveTargetEntityListener



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



    如果没有目标实体,整个配置会自动崩溃,因为占位符对象不是一个实体(并且不在/ Entity命名空间中),理论上可以将它们链接到一个不真正做任何事情的Mock实体。但是这个实体然后得到自己的表(并且被查询),打开一个全新的蠕虫病毒。


  2. strong>



    对于ContactInfo,最有意义的是,用户成为拥有者,使ContactInfo成为拥有方成功避开可选部分依赖关系,只要只有涉及两捆。但是,一旦第三(也可选)捆绑包希望使用ContactInfo链接(可选),ContactInfo的拥有端就会在第三个捆绑包上从ContactInfo创建一个硬依赖。



    使用户拥有的一面是合乎逻辑的,是一个特定的情况。然而,问题是通用的,其中实体A包含B,C包含B。


  3. 使用单表继承



    只要可选bundle是与新添加的关联交互的唯一bundle,给每个bundle自己的扩展UserBundle\Entities\User的User实体就可以工作。然而,具有扩展单个实体的多个捆绑包迅速导致这变得有点混乱。您绝对不能完全确定哪些功能可用,并且控制器以某种方式响应捆绑和/或关闭(如Symfony 2的依赖注入机制所支持的)几乎不可能。


欢迎任何有关如何规避此问题的想法或见解。经过几天的闯入砖墙,我是新鲜的想法。人们会期望Symfony有一些这样做的方法,但文档只出现了ResovleTargetEntityListener,这是次优的。

解决方案

我终于设法找到了一个适合我的项目的这个问题的解决方案。作为一个介绍,我应该说,我的架构中的捆绑布局是星星般的。我的意思是说,我有一个核心或基础的捆绑作为基础依赖模块,并存在于所有的项目。所有其他捆绑包都可以依赖它,只能依靠它。我的其他软件包之间没有直接的依赖关系。我相信这个提出的解决方案将在这种情况下工作,因为架构的简单性。我也应该说,我担心这种方法可能涉及到调试问题,但是可以根据配置设置轻松地打开或关闭这个方法。



基本思想是设置自己的ResolveTargetEntityListener,如果相关实体丢失,它将跳过相关实体。如果有一个绑定到该接口的类丢失,这将允许执行的进程继续。可能没有必要强调错误在配置中的含义 - 该类不会被发现,这可能会产生难以调试的错误。这就是为什么我建议在开发阶段将其关闭,然后在生产中重新开始。这样,所有可能的错误都将由Doctrine指出。



实施



实现包括重新使用ResolveTargetEntityListener的代码,并在 remapAssociation 方法中添加一些其他代码。这是我的最终实现:

 <?php 
命名空间Name\MyBundle\Core;

使用Doctrine\ORM\Event\LoadClassMetadataEventArgs;
使用Doctrine\ORM\Mapping\ClassMetadata;

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

/ **
*添加一个目标实体类名来解析为一个新的类名。
*
* @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;
}

/ **
*处理事件并解析新的目标实体名称。
*
* @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);
}
}
}

私有函数remapAssociation($ classMetadata,$ mapping)
{
$ newMapping = $ this-> resolveTargetEntities [$映射[ 'targetEntity']];
$ newMapping = array_replace_recursive($ mapping,$ newMapping);
$ newMapping ['fieldName'] = $ mapping ['fieldName'];

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

//如果相关实体缺少
,则静默地跳过关联if(class_exists($ newMapping ['targetEntity'])=== false)
{
返回
}

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,则不会丢失方法错误)。



使用它使用



为了能够使用这个代码,你只需要更改一个参数。你应该把这个更新的参数放到一个永远被加载的服务文件或者其他类似的地方。目标是让它在一个永远被使用的地方,无论你打算使用什么包。我把它放在我的基本捆绑服务文件中:

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

这将将原始的ResolveTargetEntityListener重定向到您的版本。



测试



我可以把你的缓存放在适当的位置只做了几个简单的测试,证明这种方法可以按预期工作。我打算在接下来的几周内经常使用这种方法,如果有需要,我将会跟进。我也希望得到一些有用的反馈意见,其他人决定放手。


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.

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.

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's ResolveTargetEntityListener

    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)

    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.

  2. Inverse the relation

    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.

    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.

  3. Use single-table inheritance

    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.

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 ResovleTargetEntityListener, 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.

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.

Implementation

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;
        }
    }
}

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).

Putting it to use

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

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.

Testing

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天全站免登陆