Dans cet exemple nous allons prendre le cas concret d’une
interface simple qui comprend des éléments
multiples.
Nous avons choisis d’utiliser une table
« Projets » dans laquelle nous allons
affecter des documents.
Donc pour un projet, nous pouvons
affecter plusieurs documents. Et pour faciliter
l’administration, nous allons faire en sorte de pouvoir
gérer notre interface imbriquée directement dans la
vue d’édition de notre projet.
Il nous faut
créer le schéma.
Au préalable on importe
notre schéma actuel dans MysqlWorbench avec la fonction
« Database/reverse engineer ». Ce qui nous
permettra d’avoir les tables par défaut de Sonata
puisque nous allons jouer avec les médias.
Avec MysqlWorbench on choisit une relation 1->N entre
notre table « projets » et la table
« projets_medias » et
ensuite une relation N->1 entre la table
« projets_medias » et la
table
« media__media« .
1->N
= OneToMany
N->1 = ManyToOne
La subtilité consiste à mettre un id auto incrément par défaut dans notre table de liaison et de ne pas mettre nos clés étrangères en primary.
Ça nous donne ceci :
projet_id contiendra l’id de la
table projets et media_id
contiendra l’id de la table
media__media.
On rajoute un champ ordre mais
on peut rajouter autant de champs que l’on souhaite. Car
nous allons voir comment gérer l’ensemble du
formulaire d’édition des éléments de
projets_medias.
Dans MysqlWorbench, on choisit « Forward/reverse
engineer » pour pousser notre schéma dans
mysql.
On fait un dernier tour de passe/passe avec les
commandes de Symfony pour générer nos entités
et nos interfaces d’admin par défaut.
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
C’est magique mais incomplet. Il nous faut corriger les
relations dans nos 2 entités.
Nous allons
déjà définir les propriétés
d’accès dans les deux sens.
Pour l’entité App\Entity\Projets ce sera la propriété documents, avec les accesseurs addDocument et 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;
}
Pour l’entité
App\Entity\ProjetsMedias ce sera la
propriété projets, avec les
accesseurs getProjets() et
setProjets().
private $projets;
public function getProjets(): ?Projets
{
return $this->projets;
}
public function setProjets(?Projets $projets): self
{
$this->projets = $projets;
return $this;
}
Concentrons nous sur la configuration de la relation Projets->ProjetsMedias car c’est là que tout se joue.
/**
* @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;
La propriété targetEntity de notre OnToMany doit
pointer sur l’entité de notre table qui contient la
liste des documents et on doit lui donner la
propriété de notre relation ManyToOne de cette
entité.
Donc ici c’est \App\Entity\ProjetsMedias
qui a une propriété projets. On lui
donne ensuite le nom de la table mysql qu’il doit joindre :
projets_media. JoinColumn définit le nom
du champ de la table jointe qui contiendra nos clés
primaires de projets, soit la valeur de notre
propriété id, qui doit être
stockée dans la table projets_id de
projets_media.
Ensuite nous configurons notre relation dans 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;
}
La propriété targetEntity de notre ManyToOne doit
pointer sur l’entité de notre table qui gère
les projets et on doit lui donner la propriété de
notre relation OneToMany de cette entité.
Donc ici
c’est Projets qui a une
propriété documents.
On lui
donne ensuite le champ de notre table courante (ProjetsMedia) qui
stocke la clef primaire du projet. Donc id de la
table projets qui est stockée dans
projets_id de projets_media
C’est ni plus ni moins que l’inverse de ce que l’on a configuré pour notre définition de \App\Entity\Projets::$documents
Dernière étape, comme on a choisit de jouer avec des médias, on doit configurer notre relation avec la médiathèque de Sonata et configurer les accesseurs.
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;
}
Idem que plus haut. On commence par donner l’entité cible, ici « App\Application\Sonata\MediaBundle\Entity\Media ». Car c’est l’entité que nous avons étendue dans notre configuration du bundle Sonata/Media.
Dans sonata_media.yml nous avons ceci :
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
Ça semble évident mais si vous tombez sur cet article sans avoir toutes les bases il me parait judicieux de le rappeler. Ça pourrait en débloquer plus d’un 😉
Ensuite JoinColumn définit comment on enregistre la relation. Donc c’est le champ media_id de notre table projets_media qui contiendra le champ id de notre table qui gère les médias (media__media).
Il nous reste à configurer le champs média de notre
admin que nous avons généré automatiquement
plus haut (ProjetsMediasAdmin.php).
Pour gérer un champ
média il faut juste utiliser le ModelListType de Sonata.
Libre à vous d’activer les boutons optionnels
ajout/edition/liste/delete.
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')
;
}
Les relations sont maintenant complètes. On peut vérifier que tout fonctionne en activant notre admin de ProjetsMedia.
Dans nos services la génération de nos interfaces nous a configuré ceci :
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, ~]
On peut donc aller directement sur notre interface pour voir si ça fonctionne.
Mais ce qui nous intéresse c’est d’avoir cette interface de gestion directement dans notre gestion de Projets.
Et pour cela nous allons utiliser les
CollectionType de Sonata.
Nous ajoutons
à notre admin généré automatiquement,
dans la méthode qui gère la configuration de
formulaire d’édition (configureFormFields), un champ
documents qui aura comme type
CollectionType avec sa configuration, et chose la
plus importante, on ajoute le paramètre
admin_code qui pointe sur le nom du service de
notre admin de projets_media.
$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'
]
);
Accessoirement, dans \App\Admin\ProjetsMediasAdmin, on supprime notre champ projets puisque la valeur sera renseignée automatiquement lorsque l’admin sera imbriqué dans projets.
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')
;
}
Et la magie opère. On peut maintenant ajouter/supprimer des médias directement dans notre interface.
Mais ce n’est pas tout à fait terminé. On a un champ pour déterminer l’ordre de notre affichage. Il nous faut juste configurer l’admin de ProjetsMedia pour modifier le type du champs ordre.
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)
;
}
Ensuite on configure notre Collection dans Projets, pour que le paramètre sortable pointe sur notre bon champ.
$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'
]
);
Et dernier point, on modifie notre relation Projets->ProjetsMedia pour donner l’ordre souhaité.
/**
* @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;
Notre collection peut maintenant être ordonnée par simple drag’n drop.