Sortable Sonata Type Model in Admin
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
andExpectation
Entities classes. - you already have an
UserAdmin
andExpectationAdmin
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 ofExpectation
to be displayed, that's explain the use ofquery
. - We want to persist
UserHasExpectations
entities, but we manageExpectation
, 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;
}
}