Заполнение свойств из Embedded другого документа
// 18 декабря, 2012 // Doctrine 2
На днях столкнулись с коллегой с одним занимательным багом. По какой-то причине не заполняся Embedded документ в Doctrine ODM. При всём при этом, когда мы его дампили, то он исправно показывался, а вот до самой MongoDB так и не доходил.
Проблема
Суть баги можно выразить вот таким псевдокодом.
/* @var Doctrine\ODM\MongoDB\DocumentManager */ $a = $dm->getRepository('Document\SomeDoc')->findOneBy(array('id' = > '50a5468f0d42e2344800003c'); $b = $dm->getRepository('Document\AnotherDoc')->findOneBy(array('id' = > '50a5468f0d42e2344800003c'); $b->setSomeEmbeddedOneField($a->getSomeEmbeddedOneField());
После этого при дампе объекта $b в нём в свойстве someEmbeddedOneField лежит наш Embedded документ, а вот при сохранении — ничего не сохраняется.
$dm->persist($b); $dm->flush();
Причина проблемы
После долгих ночных изысканий выяснилось, что проблема кроется в том, что доктрина при сохранении документов проходится по их дереву, от родителя к потомкам (Embedded). Когда она добирается до нашего Embedded документа, то получает ссылку на Embedded из объекта a. А ведь он то в базе сохранён и пересохранять его не надо. Поэтому нет запроса. В PHP объекты передаются по ссылке, вот и получается, что у нас в объекте b в поле someEmbeddedOneField стоит ссылка на Embedeed объекта а.
Возможные решения
Костыль
$this->dm->getUnitOfWork()->clear('Document\SomeEmbeddedDocument');
Решение получше
$с = new Document\SomeEmbeddedDocument(); $c->fromArray($a->getSomeEmbeddedOneField->toArray(); $b->setSomeEmbeddedOneField($ c);
Решение ещё лучше
Просто клонируем объект, полученный из другого документа.
/* @var Doctrine\ODM\MongoDB\DocumentManager */ $a = $dm->getRepository('Document\SomeDoc')->findOneBy(array('id' = > '50a5468f0d42e2344800003c'); $b = $dm->getRepository('Document\AnotherDoc')->findOneBy(array('id' = > '50a5468f0d42e2344800003c'); $b->setSomeEmbeddedOneField(clone $a->getSomeEmbeddedOneField());
А теперь, правильное решение
У предыдушего варианта есть один недостаток. Он клонирует все объекты не разбирая, новые они (клонирование в этом случчае не нужно) или уже есть в БД (клонирование нужно). Поэтому надо узнавать, а сохранён ли объект в БД, т.е. является ли он managed в терминологии Doctrine. Это можно сделать так:
/** * Проверяет, есть ли в БД уже данный экземпляр Embedded Document * Если есть, то клонирует его. Это предотвращает проблему с записями embedded в документ, при том, * что этот embedded только что взят из другого документа, и есть в БД и не менялся. * @static * @param $document */ public static function getEmbeddedForSetter($document) { if(self::$dm->contains($document)) return clone $document; return $document; }
А в мутатор добавить вызов этой функции.
public function setSomeEmbeddedField($document) { $this->someEmbeddedField = getEmbeddedForSetter($document); }
Да, функцию getEmbeddedForSetter лучше убрать в ваш сервисный слой.
Ссылки
EntityState (это про ORM на самом деле, но прочитать стоит)
Спасибо!
Если вам помогла статья, или вы хотите поддержать мои исследования и блог - вот лучший способ сделать это:
Спасибо огромное, очень помогли. Долго не мог понять, почему не сохраняются Embedded документы. Теперь все стало намного понятнее.
Рад помочь, сам с этими граблями долго просидел)