Software DevelopmentSymfony

Sonata – OneToMany Entities in Admin Forms

If you want to skip directly to the steps on how to include OneToMany relationship field in your Sonata admin form – click here.

Sonata is a really powerful open-source bundle available for your Symfony applications. One of the bundles I use most often is their Admin bundle. It will help you quickly create an administration panel for your custom application. Simply extend their AbstractAdmin class and define some options for the administration panel.

This is a simple admin controller class example:

<?php
namespace App\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;

class GroupAdmin extends AbstractAdmin
{
    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper->add('sport');
        $formMapper->add('name', TextType::class);
    }

    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper->add('sport');
        $datagridMapper->add('name');
    }

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper->add('sport');
        $listMapper->addIdentifier('name');
    }
}

This simple code snippet will create a fully functional admin panel with CRUD functionality and filtering. This admin controller is rendered as seen in the image below after setting up the menu entries.

Implementing Entity Relations

Sonata has an extensive support for including relation management fields in your administration panel forms. However,  I find the documentation on this to be very confusing and my search attempts often end-up futile.

Managing simple ManyToOne relations is simple. You can use the ChoiceType field type to present the user with a drop-down menu showing one of the attributes of the entity you want to attach. The example of this would be showing a drop-down of post categories where a user can pick one to be associated with the post.

Sonata handles this really well and will work most of the time with minimal configuration changes. The problem, however, comes in when you try to include more complex relations inside your admin controller. How can I add a small widget in my admin form that will allow me to create/add or remove related entities?

 

Embedding OneToMany Relationship Editors in Admin Controllers

When you check the Sonata documentation you are quickly made aware that there is a CollectionType that should do the job. The CollectionType field allows you to embed a small CRUD form that allows you to create and delete child objects for your entity.  When you first instantiate a CollectionType field for your OneToMany field, you will be negatively surprised that it looks something like this when rendered:

This would be completely fine if my Score entity only has a text field that I have to enter. But it doesn’t, it has a relation to the College entity and an integer score. Both of which I can’t just type into a single text field. For my use-case, I want to have a College dropdown and a number field for each entry.

All of my search attempts ended up with suggestions to implement the form myself. This is an approach I find really odd since the whole point of Sonata Admin bundle is to get rid of those repetitive tasks.

When you dig a bit deeper into the implementation of the actual Admin bundle you will see there is a support for custom form types for each collection entry. I have however yet to find this mentioned somewhere in the documentation. The whole reason for writing this post is trying to document this for my future self and anyone else that has this problem in the future.

 

Now that you survived the backstory, let’s get to the actual solution.

 

The Nitty Gritty

Example scenario: I have an Event entity that has multiple Scores recorded for each event. Each Score corresponds to a College and has a certain integer score recorded for that event.

Use-case: Administrator opens the Event edit page. -> Is able to record all scores for Colleges that participated in the Event.

In the configureFormFields method of your admin controller set-up your field OneToMany field:

$formMapper->add('scores', CollectionType::class, [
            'by_reference' => false, // Use this because of reasons
            'allow_add' => true, // True if you want allow adding new entries to the collection
            'allow_delete' => true, // True if you want to allow deleting entries
            'prototype' => true, // True if you want to use a custom form type
            'entry_type' => ScoreType::class, // Form type for the Entity that is being attached to the object
        ]);

By defining the prototype and entry_type properties a form for that type of Entity will be shown instead of the default text field.

Now the form rendering will be as you would expect. However, the changes are not persisted when you save the entity.

To handle the persistence problem you just need to override the parent AbstractAdmin prePersist and preUpdate events. The prePersist event triggers when you create a new entity, and preUpdate when you modify an existing one.

public function prePersist($object)
{
    foreach ($object->getScores() as $score) {
        $score->setCollege(new College());
        $score->setEvent($object);
    }
}

public function preUpdate($object)
{
    foreach ($object->getScores() as $score) {
        $score->setEvent($object);
    }
}

This will assign the Event entity we are editing to the added/modified Score entities.

Voila! All done, you will now have a simple to manage form without doing all of the dirty work yourself.