Исходные данные:
Есть доктриновская сущность User и доктриновский документ Address. Между ними должна быть установлена связь один-ко-многим. Всё это должно управляться с формы добавления пользователя в админке на базе сонаты. Поскольку у 1 юзера может быть много адресов, на форме добавления пользователей должна быть реализована коллекция форм добавления адресов с кнопками «добавить», «удалить» и inline редактированием полей связанных адресов. Этим мы и займёмся далее.
Что нам надо:
1) Установить @Gedmo\References doctrine-extension
Это нужно, чтобы мы могли получать коллекцию связанных адресов для заданного юзера из монго, и наоборот — привязанного юзера к каждому адресу из mysql.
Пишем в composer.json:
"gedmo/doctrine-extensions": "dev-master"
обновляем зависимости.
Установятся все doctrine-extensions, но нам нужен только один — конкретно References, предназначенный для связи между сущностями и документами.
Подробнее о нём здесь: github.com/Atlantic18/DoctrineExtensions/blob/master/doc/references.md
Теперь нам нужно прописать в config.yml 2 сервиса, обрабатывающие обе стороны связей.
Вы можете вынести эти конфиги в отдельный файл, скажем, в doctrine_extensions.yml и потом подключить его в config.yml, если вы пользуетесь ещё какими-то доктриновскими расширениями.
services: gedmo.listener.reference: class: Gedmo\References\ReferencesListener tags: - { name: doctrine_mongodb.odm.event_subscriber } calls: - [ setAnnotationReader, [ "@annotation_reader" ] ] - [ registerManager, [ 'entity', "@doctrine.orm.default_entity_manager" ] ] utils.listener.reference: class: Utils\ReferenceBundle\Listener\ReferencesListener arguments: ["@service_container"] tags: - { name: doctrine.event_subscriber, connection: default }
Первый сервис настраивает вендорный listener. С ним работает manyToOne сторона. (getUser() метод в Address документе). А для стороны oneToMany нужен второй сервис с кастомным listenerом.
Ниже привожу класс Utils\ReferenceBundle\Listener\ReferencesListener, который следует положить в тот бандл, где находятся ваши глобальные хелперы и утилиты.
<?php namespace Utils\ReferenceBundle\Listener; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Class ReferencesListener * * @package Utils\ReferenceBundle\Listener */ class ReferencesListener extends \Gedmo\References\ReferencesListener { /** * @var \Symfony\Component\DependencyInjection\ContainerInterface */ private $container; /** * @var array */ protected $managers = [ 'document' => 'doctrine.odm.mongodb.document_manager', 'entity' => 'doctrine.orm.default_entity_manager' ]; /** * @param ContainerInterface $container * @param array $managers */ public function __construct(ContainerInterface $container, array $managers = array()) { $this->container = $container; parent::__construct($managers); } /** * @param $type * * @return object */ public function getManager($type) { return $this->container->get($this->managers[$type]); } }
Примечание: существует удобный бандл, который делает за вас работу по прописыванию сервисов для листенеров доктрин-экстеншнов — вот этот: Stof\DoctrineExtensionsBundle ( github.com/stof/StofDoctrineExtensionsBundle ), но в нём нет реализации именно для References экстешна, поэтому приходится писать самому и его я здесь не использую.
Теперь нужно прописать соответствующие аннотации для полей вашей сущности и документа. При этом нужно предусмотреть поле в монго с user_id для внешнего ключа, поскольку самостоятельно это поле в монго не создастся.
/*Entity\User:*/ /** * @var ArrayCollection * * @Gedmo\ReferenceMany(type="document", class="\Application\Sonata\UserBundle\Document\Address", mappedBy="user") */ protected $addresses;
/*Document\Address:*/
/**
* @Gedmo\ReferenceOne(type=«entity», class="\Application\Sonata\UserBundle\Entity\User", inversedBy=«addresses», identifier=«user_id», mappedBy=«user_id»)
*/
protected $user;
/**
* var int $user_id
*/
protected $user_id;
Сеттеры\Геттеры для данных классов я пока не привожу, о них пойдёт речь дальше. Типы полей у меня смапплены в yaml конфигах, а как прописывать гедмо референсы в ямле я так и не разобрался. Буду благодарен, если укажете это в комментариях.
После вышеприведённых настроек у вас должно всё работать почти так, как-будто перед вами обычная связь one-to-many между двумя сущностями или документами, за исключением того, что подобный код работать не будет:
$user = new User(); $address = new Address(); $address->setAddress(«aaa»); $address->setUser($user); $user->getAddresses()->add($address); $em->persist($user); $em->flush();
Вместо этого нужно явно перзистить каждый адрес доктриновским документ-менеджером. Эту проблему я пока не решил.
2. Приступим к рендеру формы для добавления пользователей с привязанной к ней коллекцией адресов.
Внутри вашего UserAdmin класса:
protected function configureFormFields(FormMapper $formMapper) { $formMapper ->with('General') // …всякие поля ->add('addresses', 'collection', array('type' => new AddressType(), 'allow_add' => true, 'by_reference' => false, 'allow_delete' => true)) ->end(); }
Обратите внимание, что здесь мы используем обычную симфониевскую коллекцию (подробнее о ней: symfony.com/doc/current/cookbook/form/form_collections.html ) вместо sonata_type_collection, которую привязать к монго не получилось вообще.
Для использования collection типа обязательно нужен объект формы — AddressType в нашем случае. Сделаем форму. Обычную симфонивскую форму.
class AddressType extends AbstractType { /** * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('firstname') ->add('lastname') ->add('address') ; } /** * @param OptionsResolverInterface $resolver */ public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Application\Sonata\UserBundle\Document\Address' )); } /** * @return string */ public function getName() { return 'application_sonata_userbundle_address'; // и так далее.... */
Обязательно следует задать дефолтную настройку data_class с полным именем класса Address со всеми неймспейсами.
В результате, на вашей форме добавления\редактирования пользователей в сонате должен появиться вот такой элемент: (при условии, что у вас уже привязана пара адресов к текущему пользователю)

Кнопка «+» — добавление блока адреса, «-» — соответственно, удаление блока с формы.
3. Обрабатываем форму.
Теперь следует заняться сеттерами сущности, которую мы сабмитим, чтобы правильно работало добавление\удаление элементов из коллекции адресов в зависимости от того, что приходит из формы.
Обратите внимание, что при рендере коллекции адресов обязательно должен быть указан параметр by_reference=false, поскольку именно от него зависит будет ли вызван сеттер setAddresses() или добавление\удаление записей будет осуществляться где-то внутри с помощью строк типа getAddress()->add(), getAddress()->remove(). Нам такого не нужно, нам нужно, чтобы вызывался сеттер и мы могли переопределять его поведение.
Вот сам сеттер:
public function setAddresses($addresses) { foreach ($this->addresses as $orig_address) { //если на форме был удалён какой-то из существующих адресов — удалить из коллекции if (false === $addresses->contains($orig_address)) { // отсоединяем адрес от пользователя $this->addresses->removeElement($orig_address); } } //если засабмичены новые адреса, которых нет в базе, то их надо добавить в коллекцию. foreach($addresses as $passed_address) { if(!$this->addresses->contains($passed_address)) { $passed_address->setUser($this); $this->addresses->add($passed_address); } } }
Должен быть ещё метод addAddress для добавления одного адреса к существующей коллекции с привязкой к текущему юзеру:
public function addAddress($addresses) { $addresses->setUser($this); $this->addresses[] = $addresses; return $this; }
Теперь, если включить режим дебага, будет видно, что внутри коллекции addresses всё хорошо, но адреса в монго всё равно не пишутся. Это из-за описанного выше бага с тем, что не перзистится в монго коллекция. Чтобы записать адреса в монго вручную, а также удалить оттуда те адреса, которые не нужны, привяжемся к событию postUpdate() нашего UserAdmin класса:
public function postUpdate($user) { $dm = $this->container->get("doctrine_mongodb")->getManager(); $dbAddresses = $dm->getRepository('Application\Sonata\UserBundle\Document\Address')->findBy(array('user_id'=>$user->getId())); foreach($dbAddresses as $dbAddress) { if(!$user->getAddresses()->contains($dbAddress)) { echo $dbAddress->getFirstName(); $dm->remove($dbAddress); } } foreach($user->getAddresses() as $address) { $address->setUser($user); $dm->persist($address); } $dm->flush(); }
Остаётся последняя проблема — в контексте класса UserAdmin неоткуда взять documentManager для doctrine_mongodb. Это решается инъекцией сервис-контейнера в UserAdmin класс с помощью вызова сеттера контейнера из сонатовского сервиса при инициализации.
В конфиге сервисов вашего Admin класса:
sonata.user.admin.user: class: %sonata.user.admin.user.class% tags: - { name: sonata.admin, manager_type: orm, group: %sonata.user.admin.groupname%, label: users, label_catalogue: SonataUserBundle, label_translator_strategy: sonata.admin.label.strategy.underscore } arguments: - ~ - %sonata.user.admin.user.entity% - %sonata.user.admin.user.controller% calls: - [ setUserManager, [@fos_user.user_manager]] - [ setTranslationDomain, [%sonata.user.admin.user.translation_domain%]] - [ setContainer, [@service_container]]</code> нужно добавить строку <code>- [ setContainer, [@service_container]]
Затем внутри админ класса объявить новое поле container и сделать для него сеттер, который будет вызываться сервисом при инициализации класса.
/** @var \Symfony\Component\DependencyInjection\ContainerInterface */ private $container; public function setContainer (\Symfony\Component\DependencyInjection\ContainerInterface $container) { $this->container = $container; }
На этом вроде бы всё. Адреса должны добавляться, редактироваться и удаляться также, как если бы это были две обычные сущности в mysql или два обычных документа в монго.
ссылка на оригинал статьи http://habrahabr.ru/post/228371/
Добавить комментарий