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

In the process of developing an online store, the task was to implement an address book for an authorized user. So that the user 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 panel based on SonataAdminBundle.

Initial data:


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

What do 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 - a bound user to each address from mysql.

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

update dependencies.

All doctrine-extensions will be installed, but we only need one - specifically References, designed to communicate between entities and documents.
More about it here: github.com/Atlantic18/DoctrineExtensions/blob/master/doc/references.md

Now we need to register in 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 any 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 sets up a vendor listener. ManyToOne side works with it. (getUser () method in the Address document). And for oneToMany side, you need a second service with a custom listener.

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

 '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 job of writing services for doctrine extension listeners for you — this one: Stof \ DoctrineExtensionsBundle ( github.com/stof/StofDoctrineExtensionsBundle ), but it doesn’t have an implementation specifically for References, so you have to write it yourself and I don’t use it here.

Now you need to register 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 will not be created in mongo on its own.

/*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 bring, they will be discussed later. The types of fields I have mapped in yaml configs, but I still have not figured out how to register the gedmo references in yaml. I would be grateful if you indicate this in the comments.

After the above settings, everything should work for you almost as if you have before you the usual one-to-many relationship between two entities or documents, 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 must explicitly persist each address with a doctrinal document manager. I have not solved this problem yet.

2. We proceed to render the form for adding users with a collection of addresses associated with 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, you definitely need a form object - AddressType in our case. Let's make a form. The usual symphonic 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 fully qualified name of the Address class with all namespaces.

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

image

“+” button - add an address block, “-” - respectively, delete a block off form.

3. We process the form.


Now we should deal with the setters of the entity that we submit so that the addition / removal of elements from the address collection works correctly, depending on what comes from the form.
Please note that when rendering a collection of addresses, the by_reference = false parameter must be specified, since it depends on it whether the setAddresses () setter will be called or adding / deleting records will be done somewhere inside using strings like getAddress () -> add (), getAddress () -> remove (). We do not need this, we need the setter to be called, and we can redefine 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 must also be an 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, it will be seen that everything is fine inside the addresses collection, but addresses in Mongo are still not written. This is due to the bug described above so that the collection does not persist in the mongo. To write addresses in mongo manually, and also to remove those addresses that are not needed from there, we attach 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 Sonata service during initialization.

In the services config of your Admin class:

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]]
need to add a line 
- [ 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 when the class is initialized.
/** @var \Symfony\Component\DependencyInjection\ContainerInterface */
    private $container;
    public function setContainer (\Symfony\Component\DependencyInjection\ContainerInterface $container) {
        $this->container = $container;
    }


That seems to be all. Addresses should be added, edited and deleted as if they were two ordinary entities in mysql or two ordinary documents in mongo.

Also popular now: