16

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

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

但是,创建从 User 实体到 ContactInfo 实体的关联映射会创建对 ContactInfoBundle 的硬依赖,一旦禁用捆绑包,Doctrine 就会抛出 ContactInfo 不在其任何注册名称空间内的错误。

我的调查发现了几种应该解决这个问题的策略,但它们似乎都没有完全发挥作用:

  1. Doctrine 2 的 ResolveTargetEntityListener

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

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

  2. 反转关系

    对于 ContactInfo 来说,让 User 成为拥有方是最有意义的,只要只涉及两个捆绑包,让 ContactInfo 成为拥有方就成功地回避了依赖项的可选部分。但是,一旦第三个(也是可选的)捆绑包需要与 ContactInfo 的(可选)链接,则让 ContactInfo 成为拥有方会在第三个捆绑包上创建来自 ContactInfo 的硬依赖。

    使用户成为合乎逻辑的拥有方是一种特定情况。然而,当实体 A 包含 B,而 C 包含 B 时,这个问题是普遍存在的。

  3. 使用单表继承

    只要可选包是唯一与新添加的关联交互的包,为每个包提供自己的扩展 UserBundle\Entities\User 的用户实体就可以工作。然而,拥有多个扩展单个实体的捆绑包会迅速导致这变得有点混乱。你永远无法完全确定哪些功能在哪里可用,并且让控制器以某种方式响应包的打开和/或关闭(正如 Symfony 2 的 DependencyInjection 机制所支持的那样)在很大程度上是不可能的。

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

4

2 回答 2

8

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

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

执行

该实现包括重用 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 重定向到您的版本。以防万一,您还应该在将缓存放置到位后对其进行清理和加热。

测试

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

于 2013-10-20T16:33:57.413 回答
0

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

如果要为 $user 对象创建 ContactInfo,则需要手动实例化它并简单地 setEntityName(get_class($user)), setObjectId($user->getId())。要检索用户 ContactInfo 或任何对象的信息,您可以创建一个接受 $object 的通用函数。它可以简单地返回 ...findBy(array('entityName' => get_class($user), 'objectId' => $object->getId());

使用这种方法,您仍然可以使用 ContactInfo 创建用户表单(将 ContactInfo 嵌入到用户中)。尽管在处理完表单之后,您需要先持久化 User 并刷新,然后再持久化 ContactInfo。当然,这只对新创建的 User 对象是必需的,只是为了获取用户 ID。如果您担心数据完整性,请将所有持久化/刷新都放入事务中。

于 2013-07-09T16:36:20.833 回答