Пару слов про Doctrine 2 Identity Map
Ну что мои траварищи, эту заметку я пишу после тяжелого трудового дня в поисках одного коварного бага. Было тяжело, но интересно. Я познал, так сказать, прелести внутреннестей и внутренности прелестей Doctrine 2 ODM.Дело было так. Есть страничка, как полагается контроллер, экшен, вид. В виде вызывается несколько блоков. Понадобилось мне получить в контроллере ещё кое какие данные, а именно MongoId одного объекта по его ID из MySQL. В контроллере делаем запрос:
1 2 |
$qb = $this->_dm->getRepository('App\DefaultBundle\Document\SomeDoc')->createQueryBuilder(); $result = $qb->select('id')->field('sqlId')->equals($sqlId)->getQuery() |
Вроде бы всё просто. Получаем MongoId по SqlId. Однако после этого почему-то падал один из блоков. Не буду вдаваться в подробности, расскажу лишь, что у нас у документа SomeDoc есть параметр slug.
1 2 |
/** @ODM\String @ODM\Index */ protected $slug; |
Вот с такими аксессорами и мутаторами.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public function getSlug() { if(!$this->slug){ $this->setSlug(); } return $this->slug; } public function setSlug($slug = '') { if (empty($slug)) { $slug = $this->name; } $this->slug = strtolower(\Service::translit($slug)); return $this; } |
Идея то была хорошаая. Можно вызовом $someDoc->getSlug() получать slug, а если его нет – то автоматически генерить и возвращать. Но это всё в теории… А на практике….
Засада
В основном контроллере я получал объект SomeDoc, потом в виде блока делал вызов getSlug() а ещё дальше в коде выполнялось сохранение объекта. А засада состоит в том, что код работает примерно так:
- Получаем документ по sqlId. Сохраняем в IdentityMap. Это вроде кэша доктрины для одинаковых сущностей в контексте запроса. При этом, у этого объекта заполнено только поле id. Мы ведь не просили другие 🙂
- В виде вызываем getSlug(), хм, ну надо же слага нет, заполняем его пустой строкой.
- Сохраняем.
PROFIT!EPIC FAIL!
В разных местах юзается один и тот же объект, а т.к. он уже был загружен ранее – то второй вызов не грузит его. Проверить это можно сохранив первый объектв например в Zend_Registry, и сдампив его второй раз потом.
Решение
Ну понятно, что надо модифицировать вызов getSlug(), чтобы он нам такую вот заподляночку не делал. Я долго копался в Doctrine и сделал так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public function getSlug() { /* Если загрузить объект без поля slug, а потом где-то вызовется аксессор getSlug() например в виде * а потом объект где-то сохранится, то получим пустой slug без этой строчки * при этом необязательно его напарямую пеердавать, это может быть по прямой ссылке */ /** @var $dm \Doctrine\ODM\MongoDB\DocumentManager */ $dm = \AppKernel::getKernel()->getContainer()->get('doctrine_mongodb.odm.default_document_manager'); /* * Делать обновление данных из БД только для обычных документов (не Embedded) */ /** @var $metadata \Doctrine\ODM\MongoDB\Mapping\ClassMetadata */ $metadata = $dm->getClassMetadata(__NAMESPACE__.'\\'.$this->getClassName()); $isManaged = ($dm->getUnitOfWork()->getDocumentState($this) === \Doctrine\ODM\MongoDB\UnitOfWork::STATE_MANAGED); if(!$metadata->isEmbeddedDocument && $isManaged) $dm->refresh($this); if(!$this->slug){ $this->setSlug(); } return $this->slug; } |
Иными словами мы при вызове getSlug() просто догружаем данные из БД в случае, когда документ в состоянии MANAGED (получен из БД, а не инстанцирован в коде) и если он не Embedded. После обновления геттеров и сеттеров не забудьте перегенерить прокси-классы.
Используй события, Люк, и да прибудет с тобою сила!
Костыль начался еще с “$this->slug = strtolower(\Service::translit($slug));” в getSlug(), и явно проявил себя при необходимости заюзать EM в сущности.
Валер, а распиши подробнее плиз.
Или заюзать стандарный Sluggable из DoctrineExtensions (если подходит по функционалу), или же сделать свой, узкоспециализированный и попроще. Подсмотреть реализацию некоторых моментов можно, например, здесь https://github.com/l3pp4rd/DoctrineExtensions/tree/master/lib/Gedmo/Sluggable