Как делать необязательные ассоциации с перекрестными связями в Symfony 2? - программирование
Подтвердить что ты не робот

Как делать необязательные ассоциации с перекрестными связями в Symfony 2?

Я работаю над проектом Symfony 2.3, который использует ORM Doctrine 2. Как и следовало ожидать, функциональность разделяется и группируется в основном независимые пакеты, чтобы допускать повторное использование кода в других проектах.

У меня есть UserBundle и ContactInfoBundle. Контактная информация отламывается, поскольку другие объекты могут иметь связанную с ними контактную информацию, однако не исключено, что система может быть построена там, где пользователи не требуют указанной контактной информации. Поэтому я бы очень предпочел, чтобы эти два не делились жесткими ссылками.

Однако создание сопоставления ассоциаций из объекта User в объект ContactInfo создает жесткую зависимость от ContactInfoBundle, как только пакет отключен. Doctrine выдает ошибки, которые ContactInfo не входит ни в одно из своих зарегистрированных пространств имен.

Мои исследования выявили несколько стратегий, которые должны противостоять этому, но ни один из них не кажется полностью функциональным:

  • Doctrine 2 ResolveTargetEntityListener

    Это работает, если интерфейс фактически заменен во время выполнения. Поскольку зависимость пакета не обязательна, вполне возможно, что нет конкретной конкретной реализации (т.е. ContactInfoBundle не загружен)

    Если не существует целевой объект, вся конфигурация сворачивается на себя, потому что объект-заполнитель не является сущностью (и не находится в пространстве имен /Entity ), теоретически можно связать их с объектом Mock, который на самом деле не делает что-нибудь. Но этот объект затем получает свою собственную таблицу (и ее спрашивают), открывая совершенно новую банку червей.

  • Обратное отношение

    Для ContactInfo наиболее важным для пользователя является принадлежность к стороне, что делает ContactInfo собственной стороной успешно обойти необязательную часть зависимости, если задействованы только два пакета. Однако, как только третий (также необязательный) комплект желает (необязательной) ссылки с ContactInfo, создавая ContactInfo, сторона-владелец создает жесткую зависимость от ContactInfo на третьем комплекте.

    Создание логической стороны, являющейся логической стороной, является конкретной ситуацией. Однако проблема универсальна, когда объект A содержит B, а C содержит B.

  • Использовать наследование с одной таблицей

    До тех пор, пока необязательные связки являются единственными, которые взаимодействуют с недавно добавленной ассоциацией, давая каждому пакету собственный объект пользователя, который расширяет UserBundle\Entities\User, может работать. Однако наличие нескольких пакетов, которые расширяют единый объект, быстро приводит к тому, что это становится немного беспорядочным. Вы никогда не можете быть полностью уверены в том, какие функции доступны там, где контроллеры каким-то образом реагируют на входящие и/или выключенные пакеты (как это подтверждается механизацией Symfony 2 DependencyInjection) становится практически невозможным.

Любые идеи или идеи о том, как обойти эту проблему, приветствуются. После нескольких дней бегства в кирпичные стены я свеж от идей. Можно было бы ожидать, что у Symfony будет какой-то способ сделать это, но в документации есть только ResovleTargetEntityListener, что является неоптимальным.

4b9b3361

Ответ 1

Наконец-то мне удалось установить решение этой проблемы, которое было бы подходящим для моего проекта. В качестве введения я должен сказать, что пучки в моей архитектуре выложены "звездообразно". Под этим я подразумеваю, что у меня есть один основной или базовый пакет, который служит базовым модулем зависимостей и присутствует во всех проектах. Все остальные пучки могут полагаться на него и только на него. Прямых зависимостей между моими другими пакетами нет. Я вполне уверен, что это предлагаемое решение будет работать в этом случае из-за простоты архитектуры. Я также должен сказать, что я боюсь, что могут быть проблемы с отладкой, связанные с этим методом, но это может быть сделано так, что он легко включается или выключается, в зависимости от настройки конфигурации, например.

Основная идея состоит в том, чтобы настроить собственный 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, который используется для сопоставления отношений сущности. Если соответствующий класс сущностей не существует, метод просто возвращает, а не выполняет ошибочное сопоставление и создает ошибку. Это также подразумевает отсутствие поля (если это не отношение "многие-ко-многим" ). Внешний ключ в этом случае будет просто отсутствовать внутри базы данных, но, поскольку он существует в классе сущности, весь код по-прежнему действителен (вы не получите ошибку отсутствующего метода, если случайно вызываете гейтер или сеттер внешнего ключа).

Положив его на использование

Чтобы иметь возможность использовать этот код, вам просто нужно изменить один параметр. Вы должны поместить этот обновленный параметр в файл служб, который всегда будет загружен или в другое подобное место. Цель состоит в том, чтобы иметь его в месте, которое всегда будет использоваться, независимо от того, какие пакеты вы собираетесь использовать. Я поместил его в файл службы базового пакета:

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

Это приведет к перенаправлению исходного ResolveTargetEntityListener к вашей версии. Вы также должны очистить и согреть свой кеш, положив его на место, на всякий случай.

Тестирование

Я выполнил только пару простых тестов, которые доказали, что этот подход может работать так, как ожидалось. Я намерен часто использовать этот метод в ближайшие пару недель и буду следить за ним, если возникнет такая необходимость. Я также надеюсь получить полезную обратную связь от других людей, которые решили уйти.

Ответ 2

Вы можете создать свободные зависимости между ContactInfo и любыми другими объектами, добавив дополнительное поле в ContactInfo для различения сущностей (например, $entityName). Другим обязательным полем будет $objectId, чтобы указать на объекты конкретных объектов. Поэтому, чтобы связать пользователя с ContactInfo, вам не нужны какие-либо фактические реляционные сопоставления.

Если вы хотите создать ContactInfo для объекта $user, вам необходимо вручную создать его экземпляр и просто setEntityName (get_class ($ user)), setObjectId ($ user- > getId()). Чтобы получить пользователя ContactInfo или объект любого объекта, вы можете создать универсальную функцию, которая принимает $object. Он может просто просто вернуться... findBy (array ('entityName' = > get_class ($ user), 'objectId' = > $object- > getId());

При таком подходе вы все равно можете создать форму Пользователя с ContactInfo (вставить ContactInfo в User). Хотя после того, как вы обработаете форму, вам нужно будет перенести пользователя сначала и выполнить флеш, а затем сохранить ContactInfo. Конечно, это необходимо только для вновь созданных объектов пользователя, чтобы получить идентификатор пользователя. Поместите все persist/flush в транзакцию, если вы обеспокоены целостностью данных.