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