In this example, we will take the concrete case of a simple
interface that includes multiple elements.
We have chosen to
use a "Projects" table in which we will assign documents.
Therefore, for a project, we can assign several documents. And
to facilitate administration, we will ensure that we can manage
our nested interface directly within the edit view of our
project.
We need to create the schema.
Firstly, we
import our current schema into MysqlWorbench using the
"Database/reverse engineer" function. This will allow us to have
the default tables of Sonata since we are going to play with
media.
With MysqlWorbench, we choose a 1->N relationship between
our "projects" table and the "projects_medias" table and then an
N->1 relationship between the "projects_medias" table and the
"media__media" table.
1->N = OneToMany
N->1 = ManyToOne
The subtlety consists in putting a default auto-incrementing id in our linking table and not to have our foreign keys as the primary.
This is what we get:
projet_id will contain the id from the
projects table and media_id will
contain the id from the media__media table.
We
add an order field but we can add as many fields as we wish.
Because we are going to see how to manage the entire form for
editing the elements of projects_medias.
In MysqlWorbench, we choose "Forward/reverse engineer" to push our
schema into MySQL.
We do a final trick with the Symfony
commands to generate our entities and our default admin
interfaces.
bin/console doctrine:mapping:import "App\Entity" annotation --path=src/Entity
bin/console make:entity --regenerate App
bin/console make:sonata:admin App/Entity/Projets
bin/console make:sonata:admin App/Entity/ProjetsMedias
It's magical but incomplete. We need to correct the relationships
in our 2 entities.
We will already define the access properties
in both directions.
For the entity App\Entity\Projets it will be the documents property, with the accessors addDocument and removeDocument
use Doctrine\Common\Collections\Collection;
...
...
private $documents;
public function getDocuments(): Collection
{
return $this->documents;
}
public function addDocument(ProjetsMedias $document): self
{
if (!$this->documents->contains($document)) {
$this->documents[] = $document;
$document->setProjets($this);
}
return $this;
}
public function removeDocument(ProjetsMedias $document): self
{
if ($this->documents->removeElement($document)) {
// set the owning side to null (unless already changed)
if ($document->getProjets() === $this) {
$document->setProjets(null);
}
}
return $this;
}
For the entity
App\Entity\ProjetsMedias it will be the
projets property, with the accessors
getProjets() and setProjets().
private $projets;
public function getProjets(): ?Projets
{
return $this->projets;
}
public function setProjets(?Projets $projets): self
{
$this->projets = $projets;
return $this;
}
Let's focus on the configuration of the Projets->ProjetsMedias relationship because this is where it all happens.
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\OneToMany(targetEntity="App\Entity\ProjetsMedias", mappedBy="projets", cascade={"persist", "remove" })
* @ORM\JoinTable(name="projets_medias",
* joinColumns={
* @ORM\JoinColumn(name="projets_id", referencedColumnName="id")
* }
* )
*/
private $documents;
The targetEntity property of our OneToMany must point to the entity
of our table that contains the list of documents and we must give
it the property of our ManyToOne relationship of this
entity.
So here it is \App\Entity\ProjetsMedias which has a
projets property. We then give it the name of the
MySQL table it must join: projets_media.
JoinColumn defines the name of the field in the joined table that
will contain our project primary keys, so the value of our
id property, which must be stored in the
projets_id of projets_media.
Then we configure our relationship in ProjetsMedia.
/**
* @var \Projets
*
* @ORM\ManyToOne(targetEntity="Projets",inversedBy="documents")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="projets_id", referencedColumnName="id")
* })
*/
private $projets;
public function getProjets(): ?Projets
{
return $this->projets;
}
public function setProjets(?Projets $projets): self
{
$this->projets = $projets;
return $this;
}
The targetEntity property of our ManyToOne must point to the entity
of our table that manages the projects and we must give it the
property of our OneToMany relationship of this entity.
So here
it is Projets which has a
documents property.
We then give it the field
of our current table (ProjetsMedia) which stores the primary key
of the project. So id from
projets table which is stored in
projets_id of projets_media
This is none other than the reverse of what we configured for our definition of \App\Entity\Projets::$documents
Last step, as we have chosen to play with media, we must configure our relationship with the Sonata media library and configure the accessors.
use Sonata\MediaBundle\Model\MediaInterface;
...
...
/**
* @var \MediaMedia
*
* @ORM\ManyToOne(targetEntity="App\Application\Sonata\MediaBundle\Entity\Media")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="media_id", referencedColumnName="id")
* })
*/
private $mediaMedia;
public function __toString(): string
{
return $this->getMediaMedia();
}
public function getMediaMedia(): ?MediaInterface
{
return $this->mediaMedia;
}
public function setMediaMedia(?MediaInterface $mediaMedia): self
{
$this->mediaMedia = $mediaMedia;
return $this;
}
Same as above. We start by giving the target entity, here "App\Application\Sonata\MediaBundle\Entity\Media". Because it's the entity that we extended in our configuration of the Sonata/Media bundle.
In sonata_media.yml we have this:
sonata_media:
class:
media: App\Application\Sonata\MediaBundle\Entity\Media
gallery: App\Application\Sonata\MediaBundle\Entity\Gallery
gallery_item: App\Application\Sonata\MediaBundle\Entity\GalleryItem
category: App\Application\Sonata\ClassificationBundle\Entity\SonataClassificationCategory
It seems obvious but if you come across this article without having all the basics, it seems wise to remind you. It could unblock more than one person. 😊
Then JoinColumn defines how we record the relationship. So it's the field media_id from our projets_media table that will contain the id field of our table that manages the media (media__media).
Now we just need to configure the media field of our admin that we
automatically generated above (ProjetsMediasAdmin.php).
To
manage a media field, you just need to use the ModelListType of
Sonata. You are free to activate the optional add/edit/list/delete
buttons.
use Sonata\AdminBundle\Form\Type\ModelListType;
...
...
protected function configureFormFields(FormMapper $form): void
{
$form
->add('projets')
->add('mediaMedia', ModelListType::class, [
'required' => false,
'btn_add'=>true,
'btn_edit'=>false,
'btn_list'=>false,
'btn_delete'=>false,
])
->add('ordre')
;
}
The relationships are now complete. We can check that everything works by activating our ProjetsMedia admin.
In our services, the generation of our interfaces has configured this for us:
admin.projets:
class: App\Admin\ProjetsAdmin
tags:
- { name: sonata.admin, model_class: App\Entity\Projets, controller: App\Controller\ProjetsAdminController, manager_type: orm, group: admin, label: Projets }
admin.projets_medias:
class: App\Admin\ProjetsMediasAdmin
tags:
- { name: sonata.admin, model_class: App\Entity\ProjetsMedias, controller: App\Controller\ProjetsMediasAdminController, manager_type: orm, group: admin, label: ProjetsMedias }
arguments: [~, App\Entity\ProjetsMediasAdmin, ~]
So we can now go directly to our interface to see if it works.
But what we are interested in is to have this management interface directly in our Projects management.
And for that we will use Sonata's CollectionType.
We add to our automatically generated admin, in the method
that manages the editing form configuration (configureFormFields),
a documents field that will have
CollectionType as its type with its
configuration, and most importantly, we add the
admin_code parameter that points to the name of
our projets_media admin service.
$form->add('documents', \Sonata\Form\Type\CollectionType::class,
[
'required' => false,
'by_reference' => false,
'label'=> false],
[
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
'link_parameters' => [
'context' => 'default',
'provider' => 'sonata.media.provider.file',
'hide_context' => true
],
'admin_code' => 'admin.projets_medias'
]
);
Incidentally, in \App\Admin\ProjetsMediasAdmin, we remove our projets field since the value will be filled in automatically when the admin is nested in projects.
protected function configureFormFields(FormMapper $form): void
{
$form
//->add('projets')
->add('mediaMedia', \Sonata\AdminBundle\Form\Type\ModelListType::class, [
'required' => false,
'btn_add'=>true,
'btn_edit'=>false,
'btn_list'=>false,
'btn_delete'=>false,
])
->add('ordre')
;
}
And magic happens. We can now add/remove media directly within our interface.
But it's not quite finished. We have a field to determine the order of our display. We just need to configure the ProjetsMedia admin to modify the type of the order field.
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
...
...
protected function configureFormFields(FormMapper $form): void
{
$form
//->add('projets')
->add('mediaMedia', ModelListType::class, [
'required' => false,
'btn_add'=>true,
'btn_edit'=>false,
'btn_list'=>false,
'btn_delete'=>false,
])
->add('ordre', HiddenType::class)
;
}
Then we configure our Collection in Projets, so that the sortable parameter points to our correct field.
$form->add('documents', \Sonata\Form\Type\CollectionType::class,
[
'required' => false,
'by_reference' => false,
'label'=> false],
[
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'ordre',
'link_parameters' => [
'context' => 'default',
'provider' => 'sonata.media.provider.file',
'hide_context' => true
],
'admin_code' => 'admin.projets_medias'
]
);
And lastly, we modify our Projets->ProjetsMedia relationship to set the desired order.
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\OneToMany(targetEntity="App\Entity\ProjetsMedias", mappedBy="projets", cascade={"persist", "remove" })
* @ORM\JoinTable(name="projets_medias",
* joinColumns={
* @ORM\JoinColumn(name="projets_id", referencedColumnName="id")
* }
* )
* @ORM\OrderBy({"ordre" = "ASC"})
*/
private Collection $documents;
Our collection can now be ordered by simple drag and drop.