Skip to content
  • About
    • What is Symfony?
    • Community
    • News
    • Contributing
    • Support
  • Documentation
    • Symfony Docs
    • Symfony Book
    • Screencasts
    • Symfony Bundles
    • Symfony Cloud
    • Training
  • Services
    • Platform.sh for Symfony Best platform to deploy Symfony apps
    • SymfonyInsight Automatic quality checks for your apps
    • Symfony Certification Prove your knowledge and boost your career
    • SensioLabs Professional services to help you with Symfony
    • Blackfire Profile and monitor performance of your apps
  • Other
  • Blog
  • Download
sponsored by
  1. Home
  2. Documentation
  3. Bundles
  4. SonataAdminBundle
  5. Sortable Sonata Type Model in Admin

Sortable Sonata Type Model in Admin

Edit this page

This is a full working example on how to implement a sortable feature in your Sonata admin form between two entities.

Background

The sortable function is already available inside Sonata for the ChoiceType. But the ModelType (or sonata_type_model) extends from choice, so this function is already available in our form type. We only need some configuration to make it work.

The goal here is to fully configure a working example to handle the following need : User got some expectations, but some are more relevant than the others.

Pre-requisites

  • you already have SonataAdmin and DoctrineORM up and running.
  • you already have a UserBundle.
  • you already have User and Expectation Entities classes.
  • you already have an UserAdmin and ExpectationAdmin set up.

The Recipe

Part 1 : Update the data model configuration

The first thing to do is to update the Doctrine ORM configuration and create the join entity between User and Expectation. We are going to call this join entity UserHasExpectations.

Note

We can't use a Many-To-Many relation here because the joined entity will required an extra field to handle ordering.

So we start by updating UserBundle/Resources/config/doctrine/User.orm.xml and adding a One-To-Many relation.

1
2
3
4
5
6
7
8
<one-to-many field="userHasExpectations" target-entity="UserBundle\Entity\UserHasExpectations" mapped-by="user" orphan-removal="true">
    <cascade>
        <cascade-persist/>
    </cascade>
    <order-by>
        <order-by-field name="position" direction="ASC"/>
    </order-by>
</one-to-many>

Then update UserBundle/Resources/config/doctrine/Expectation.orm.xml and also adding a One-To-Many relation.

1
2
3
4
5
<one-to-many field="userHasExpectations" target-entity="UserBundle\Entity\UserHasExpectations" mapped-by="expectation" orphan-removal="false">
    <cascade>
        <cascade-persist/>
    </cascade>
</one-to-many>

We now need to create the join entity configuration, create the following file in UserBundle/Resources/config/doctrine/UserHasExpectations.orm.xml.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">

    <entity name="UserBundle\Entity\UserHasExpectations" table="user__expectations">
        <id name="id" type="integer">
            <generator strategy="AUTO"/>
        </id>
        <field name="position" column="position" type="integer">
            <options>
                <option name="default">0</option>
            </options>
        </field>

        <many-to-one field="user" target-entity="UserBundle\Entity\User" inversed-by="userHasExpectations" orphan-removal="false">
            <join-column name="user_id" referenced-column-name="id" on-delete="CASCADE"/>
        </many-to-one>

        <many-to-one field="expectation" target-entity="UserBundle\Entity\Expectation" inversed-by="userHasExpectations" orphan-removal="false">
            <join-column name="expectation_id" referenced-column-name="id" on-delete="CASCADE"/>
        </many-to-one>
    </entity>
</doctrine-mapping>

Part 2 : Update the data model entities

Update the UserBundle\Entity\User.php entity with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * @var Collection|UserHasExpectations[]
 */
private Collection $userHasExpectations;

public function __construct()
{
    $this->userHasExpectations = new ArrayCollection;
}

public function setUserHasExpectations(Collection $userHasExpectations): void
{
    $this->userHasExpectations = new ArrayCollection;

    foreach ($userHasExpectations as $one) {
        $this->addUserHasExpectations($one);
    }
}

public function getUserHasExpectations(): Collection
{
    return $this->userHasExpectations;
}

public function addUserHasExpectations(UserHasExpectations $userHasExpectations): void
{
    $userHasExpectations->setUser($this);

    $this->userHasExpectations[] = $userHasExpectations;
}

public function removeUserHasExpectations(UserHasExpectations $userHasExpectations): void
{
    $this->userHasExpectations->removeElement($userHasExpectations);
}

Update the UserBundle\Entity\Expectation.php entity with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * @var Collection|UserHasExpectations[]
 */
private Collection $userHasExpectations;

/**
 * @param Collection|UserHasExpectations[] $userHasExpectations
 */
public function setUserHasExpectations(Collection $userHasExpectations)
{
    $this->userHasExpectations = $userHasExpectations;
}

/**
 * @return Collection|UserHasExpectations[]
 */
public function getUserHasExpectations(): Collection
{
    return $this->userHasExpectations;
}

/**
 * @return string
 */
public function __toString()
{
    return $this->getLabel();
}

Create the UserBundle\Entity\UserHasExpectations.php entity with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
namespace UserBundle\Entity;

class UserHasExpectations
{
    private ?int $id = null;

    private ?User $user = null;

    private ?Expectation $expectation = null;

    private ?int $position = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getUser(): ?User
    {
        return $this->user;
    }

    public function setUser(User $user): void
    {
        $this->user = $user;
    }

    public function getExpectation(): ?Expectation
    {
        return $this->expectation;
    }

    public function setExpectation(Expectation $expectation): void
    {
        $this->expectation = $expectation;
    }

    public function getPosition(): ?int
    {
        return $this->position;
    }

    public function setPosition(int $position): void
    {
        $this->position = $position;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return (string) $this->getExpectation();
    }
}

Part 3 : Update admin classes

This is a very important part, the admin class should be created for the join entity. If you don't do that, the field will never display properly. So we are going to start by creating this UserBundle\Admin\UserHasExpectationsAdmin.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace UserBundle\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;

final class UserHasExpectationsAdmin extends AbstractAdmin
{
    protected function configureFormFields(FormMapper $form): void
    {
        $form
            ->add('expectation', 'sonata_type_model', ['required' => false])
            ->add('position', 'hidden')
        ;
    }

    protected function configureListFields(ListMapper $list): void
    {
        $list
            ->add('expectation')
            ->add('user')
            ->add('position')
        ;
    }
}

... and define the service in UserBundle\Resources\config\admin.xml.

1
2
3
<service id="user.admin.user_has_expectations" class="UserBundle\Admin\UserHasExpectationsAdmin">
    <tag name="sonata.admin" model_class="UserBundle\Entity\UserHasExpectations" manager_type="orm" group="UserHasExpectations" label="UserHasExpectations"/>
</service>

Now update the UserBundle\Admin\UserAdmin.php by adding the sonata_type_model field:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected function configureFormFields(FormMapper $form): void
{
    $form
        ->add('userHasExpectations', 'sonata_type_model', [
            'label'        => 'User\'s expectations',
            'query'        => $this->modelManager->createQuery('UserBundle\Entity\Expectation'),
            'required'     => false,
            'multiple'     => true,
            'by_reference' => false,
            'sortable'     => true,
        ])
    ;

    $form
        ->get('userHasExpectations')
        ->addModelTransformer(new ExpectationDataTransformer($this->getSubject(), $this->modelManager));
}

There is two important things that we need to show here:

  • We use the field userHasExpectations of the user, but we need a list of Expectation to be displayed, that's explain the use of query.
  • We want to persist UserHasExpectations entities, but we manage Expectation, so we need to use a custom ModelTransformer to deal with it.

Part 4 : Data Transformer

The last (but not least) step is create the UserBundle\Form\DataTransformer\ExpectationDataTransformer.php to handle the conversion of Expectation to UserHasExpectations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
namespace UserBundle\Form\DataTransformer;

final class ExpectationDataTransformer implements Symfony\Component\Form\DataTransformerInterface
{
    private User $user;

    private ModelManager $modelManager;

    public function __construct(User $user, ModelManager $modelManager)
    {
        $this->user         = $user;
        $this->modelManager = $modelManager;
    }

    public function transform($value)
    {
        if (!is_null($value)) {
            $results = [];

            /** @var UserHasExpectations $userHasExpectations */
            foreach ($value as $userHasExpectations) {
                $results[] = $userHasExpectations->getExpectation();
            }

            return $results;
        }

        return $value;
    }

    public function reverseTransform($value)
    {
        $results  = new ArrayCollection();
        $position = 0;

        /** @var Expectation $expectation */
        foreach ($value as $expectation) {
            $userHasExpectations = $this->create();
            $userHasExpectations->setExpectation($expectation);
            $userHasExpectations->setPosition($position++);

            $results->add($userHasExpectations);
        }

        // Remove Old values
        $qb   = $this->modelManager->getEntityManager()->createQueryBuilder();
        $expr = $this->modelManager->getEntityManager()->getExpressionBuilder();

        $userHasExpectationsToRemove = $qb->select('entity')
                                           ->from($this->getClass(), 'entity')
                                           ->where($expr->eq('entity.user', $this->user->getId()))
                                           ->getQuery()
                                           ->getResult();

        foreach ($userHasExpectationsToRemove as $userHasExpectations) {
            $this->modelManager->delete($userHasExpectations, false);
        }

        $this->modelManager->getEntityManager()->flush();

        return $results;
    }
}
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version
    Show your Symfony expertise

    Show your Symfony expertise

    Save your teams and projects before they sink

    Save your teams and projects before they sink

    Version:

    Table of Contents

    • Background
    • Pre-requisites
    • The Recipe
      • Part 1 : Update the data model configuration
      • Part 2 : Update the data model entities
      • Part 3 : Update admin classes
      • Part 4 : Data Transformer

    Symfony footer

    Avatar of Maxime Steinhausser, a Symfony contributor

    Thanks Maxime Steinhausser (@ogizanagi) for being a Symfony contributor

    438 commits • 28.37K lines changed

    View all contributors that help us make Symfony

    Become a Symfony contributor

    Be an active part of the community and contribute ideas, code and bug fixes. Both experts and newcomers are welcome.

    Learn how to contribute

    Symfony™ is a trademark of Symfony SAS. All rights reserved.

    • What is Symfony?

      • What is Symfony?
      • Symfony at a Glance
      • Symfony Components
      • Symfony Releases
      • Security Policy
      • Logo & Screenshots
      • Trademark & Licenses
      • symfony1 Legacy
    • Learn Symfony

      • Symfony Docs
      • Symfony Book
      • Reference
      • Bundles
      • Best Practices
      • Training
      • eLearning Platform
      • Certification
    • Screencasts

      • Learn Symfony
      • Learn PHP
      • Learn JavaScript
      • Learn Drupal
      • Learn RESTful APIs
    • Community

      • Symfony Community
      • SymfonyConnect
      • Events & Meetups
      • Projects using Symfony
      • Contributors
      • Symfony Jobs
      • Backers
      • Code of Conduct
      • Downloads Stats
      • Support
    • Blog

      • All Blog Posts
      • A Week of Symfony
      • Case Studies
      • Cloud
      • Community
      • Conferences
      • Diversity
      • Living on the edge
      • Releases
      • Security Advisories
      • Symfony Insight
      • Twig
      • SensioLabs Blog
    • Services

      • SensioLabs services
      • Train developers
      • Manage your project quality
      • Improve your project performance
      • Host Symfony projects

      Powered by

    Follow Symfony