gpt4 book ai didi

symfony - 如何在 Symfony 2 中进行可选的跨捆绑关联?

转载 作者:行者123 更新时间:2023-12-03 20:54:16 25 4
gpt4 key购买 nike

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

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

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

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

  • Doctrine 2's ResolveTargetEntityListener

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

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

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

    使用户成为合乎逻辑的拥有方是一种特定情况。然而,在实体 A 包含 B,而 C 包含 B 的情况下,该问题是普遍存在的。
  • 使用单表继承

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

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

    最佳答案

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

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

    执行

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

    测试

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

    关于symfony - 如何在 Symfony 2 中进行可选的跨捆绑关联?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17545157/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com