如何在Symfony 2中进行可选的跨包关联?

时间:2013-07-09 09:48:37

标签: symfony doctrine-orm

我正在开发一个使用Doctrine 2 ORM的Symfony 2.3项目。正如预期的那样,功能被拆分并分组为大多数独立的捆绑包,以允许在其他项目中重用代码。

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

但是,创建从User实体到ContactInfo实体的关联映射会在BundIn被禁用时立即创建对ContactInfoBundle的硬依赖关系Doctrine会抛出ContactInfo不在其任何已注册命名空间内的错误。

我的调查发现了几种应对此策略的策略,但它们似乎都没有完全发挥作用:

  1. Doctrine 2's ResolveTargetEntityListener

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

    如果没有目标实体,整个配置会崩溃到自身,因为占位符对象不是实体(并且不在/ Entity命名空间内),理论上可以将它们链接到实际上并不真实的Mock实体任何东西。但是这个实体然后得到它自己的表(并且它被查询),开辟了一整套新的蠕虫。

  2. 反转关系

    对于ContactInfo,它最有意义的是User成为拥有者,使ContactInfo成为拥有者成功避开依赖的可选部分,只要涉及两个bundle。但是,只要第三个(也是可选的)bundle希望与ContactInfo建立(可选)链接,使ContactInfo成为拥有者,就会在第三个bundle上创建一个来自ContactInfo的硬依赖。

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

  3. 使用单表继承

    只要可选捆绑包是唯一与新添加的关联进行交互的捆绑包,为每个捆绑包提供自己的扩展UserBundle \ Entities \ User的用户实体即可。但是,拥有多个扩展单个实体的捆绑包会导致这种情况变得有点混乱。你永远无法完全确定哪些功能可用,并且控制器以某种方式响应打开和/或关闭的捆绑(由Symfony 2的DependencyInjection机制支持)变得非常不可能。

  4. 欢迎任何有关如何规避此问题的想法或见解。经过几天跑进砖墙后,我的想法很新鲜。人们会期望Symfony有一些方法可以做到这一点,但文档只提供了ResovleTargetEntityListener,这是次优的。

2 个答案:

答案 0 :(得分:7)

我终于设法找到了适合我项目的问题的解决方案。作为介绍,我应该说我的建筑中的束是“明星般的”。我的意思是我有一个核心或基本包作为基本依赖模块,并存在于所有项目中。所有其他捆绑包都可以依赖它而只能依赖它。我的其他捆绑包之间没有直接的依赖关系。我很确定这个提议的解决方案在这种情况下会起作用,因为架构简单。我还应该说,我担心这种方法可能存在调试问题,但可以根据配置设置轻松打开或关闭它。

基本思想是装配我自己的ResolveTargetEntityListener,如果缺少相关实体,它将跳过关联实体。如果缺少绑定到接口的类,这将允许执行过程继续。可能没有必要强调配置中的拼写错误 - 该类将无法找到,这可能会产生难以调试的错误。这就是为什么我建议在开发阶段关闭它然后在生产中重新打开它。这样,Doctrine就会指出所有可能的错误。

实施

实现包括重用ResolveTargetEntityListener的代码并在remapAssociation方法中添加一些额外的代码。这是我的最终实施:

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

使用

为了能够使用此代码,您只需更改一个参数即可。您应该将此更新的参数放入将始终加载的服务文件或其他类似的位置。我们的目标是将它放在一个永远使用的地方,无论您要使用哪种捆绑包。我把它放在我的基本捆绑服务文件中:

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

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

测试

我只做了几个简单的测试,证明这种方法可能会按预期工作。我打算在接下来的几周内经常使用这种方法,如果需要,我会跟进它。我也希望从其他决定试一试的人那里得到一些有用的反馈。

答案 1 :(得分:0)

您可以通过在ContactInfo中添加一个额外的字段来区分实体(例如$ entityName),从而在ContactInfo和任何其他实体之间创建松散的依赖关系。另一个必填字段是$ objectId,指向特定实体的对象。因此,为了将User与ContactInfo链接,您不需要任何实际的关系映射。

如果要为$ user对象创建ContactInfo,则需要手动实例化它,只需设置EntityName(get_class($ user)),setObjectId($ user-&gt; getId())。要检索用户ContactInfo或任何对象的用户,可以创建一个接受$ object的泛型函数。它可以简单地返回... findBy(array('entityName'=&gt; get_class($ user),'objectId'=&gt; $ object-&gt; getId());

使用此方法,您仍然可以使用ContactInfo创建用户表单(将ContactInfo嵌入到用户中)。虽然在处理完表单后,您需要先保留User并刷新,然后保持ContactInfo。当然,这仅对新创建的User对象是必需的,这样才能获得用户ID。如果您担心数据完整性,请将所有持久/刷新置于事务中。