📜 ⬆️ ⬇️

Link Doctrine Entity and Doctrine Document on the form in the Sonata Admin Bundle

In the process of developing an online store, the task was to implement the address book for an authorized user. So that the user himself is stored in the mysql database, and the addresses associated with it are stored in mongoDB. This task deserves special attention in terms of managing users and their address books from the admin area, based on SonataAdminBundle.

Initial data:


There is a doctrine entity User and a doctrine document Address. One-to-many relationship must be established between them. All this should be controlled from the add user form in the Sonata-based admin panel. Since a user can have many addresses, a form of adding addresses with buttons “add”, “delete” and inline editing the fields of related addresses should be implemented on the add user form. This is what we will do next.

What we need:


1) Install @Gedmo \ References doctrine-extension

This is necessary so that we can get a collection of related addresses for a given user from Mongo, and vice versa - an associated user to each address from mysql.

We write in composer.json:
"gedmo/doctrine-extensions": "dev-master"

update dependencies.
')
All doctrine-extensions will be installed, but we need only one - specifically References, intended for the connection between entities and documents.
Read more about it here: github.com/Atlantic18/DoctrineExtensions/blob/master/doc/references.md

Now we need to register in the config.yml 2 services that process both sides of the links.
You can put these configs into a separate file, say, in doctrine_extensions.yml and then connect it to config.yml, if you use some other doctrine extensions.

 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 } 


The first service configures the vendor listener. ManyToOne side works with him. (getUser () method in the Address document). And for the oneToMany side, we need a second service with a custom listener.

Below is the Utils \ ReferenceBundle \ Listener \ ReferencesListener class, which should be put into the bundle where your global helpers and utilities are located.

 <?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]); } } 


Note: there is a convenient bundle that does the work for you on prescribing services for the extension users of doctrines - this one: Stof \ DoctrineExtensionsBundle ( github.com/stof/StofDoctrineExtensionsBundle ), but there is no implementation for References Extras, so you have to write yourself and I don't use it here.

Now you need to write the appropriate annotations for the fields of your entity and document. In this case, it is necessary to provide a field in mongo with user_id for the foreign key, since this field in mongo will not be created by itself.

 /*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; 

Setters \ Getters for these classes, I still do not cite, they will be discussed further. I have types of fields in yaml configs, and I haven’t figured out how to register gedmo references in the ground. I would be grateful if you indicate this in the comments.

After the above settings, everything should work for you almost as if the usual one-to-many connection between two entities or documents is in front of you, except that such code will not work :
 $user = new User(); $address = new Address(); $address->setAddress(«aaa»); $address->setUser($user); $user->getAddresses()->add($address); $em->persist($user); $em->flush(); 


Instead, you need to clearly perzistit each address doctrin document manager. I have not solved this problem yet.

2. We proceed to the rendering of the form for adding users with a collection of addresses attached to it.


Inside your useradmin class:
 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(); } 


Please note that here we use the usual symphony collection (more about it: symfony.com/doc/current/cookbook/form/form_collections.html ) instead of sonata_type_collection, which could not be attached to the Mongo at all.

To use the collection type, a form object is required - AddressType in our case. Let's make a form. The usual symphony form.

 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'; //   .... */ 


Be sure to set the default data_class setting with the full name of the Address class with all the namespaces.

As a result, the following element should appear on your form for adding / editing users in the sonata: (assuming that you already have a pair of addresses attached to the current user)

image

Button “+” - add address block, “-” - respectively, remove the block from the form.

3. We process the form.


Now we should deal with the entity setters, which we submit, so that adding / deleting elements from the address collection, depending on what comes out of the form, works correctly.
Please note that when rendering a collection of addresses, the by_reference = false parameter must be specified, since it determines whether the setAddresses () setter is called or whether adding / deleting records will be done somewhere inside using strings of the type getAddress () -> add (), getAddress () -> remove (). We do not need this, we need the setter to be called and we can override its behavior.

Here is the setter itself:

 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); } } } 


There should be another addAddress method to add one address to an existing collection with reference to the current user:
 public function addAddress($addresses) { $addresses->setUser($this); $this->addresses[] = $addresses; return $this; } 


Now, if you enable debug mode, you will see that everything is fine inside the addresses collection, but addresses are not written to Mongo anyway. This is because of the above-described bug with the fact that it does not pergist in the Mongo collection. To write addresses to Mongo manually, and also to remove from there those addresses that are not needed, we will bind to the postUpdate () event of our UserAdmin class:
 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(); } 


The last problem remains - in the context of the UserAdmin class there is nowhere to get the documentManager for doctrine_mongodb. This is solved by injecting the service container into the UserAdmin class by calling the container setter from the Sonatov service during initialization.

In the config of your Admin class services:

 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]] 


Then, inside the class admin, declare a new container field and make a setter for it, which will be called by the service during class initialization.
 /** @var \Symfony\Component\DependencyInjection\ContainerInterface */ private $container; public function setContainer (\Symfony\Component\DependencyInjection\ContainerInterface $container) { $this->container = $container; } 


This seems to be all. Addresses must be added, edited and deleted as if they were two ordinary entities in mysql or two regular documents in Mongo.

Source: https://habr.com/ru/post/228371/


All Articles