SF6 / Sonata 5: Gestione di un'interfaccia OneToMany in un CRUD con il CollectionType di Sonata

In questo esempio, prenderemo il caso concreto di una semplice interfaccia che include diversi elementi.
Abbiamo scelto di utilizzare una tabella "Progetti" nella quale assegneremo dei documenti.
Di conseguenza, per un progetto, possiamo assegnare diversi documenti. E per facilitare l'amministrazione, ci assicureremo di poter gestire la nostra interfaccia nidificata direttamente all'interno della vista di modifica del nostro progetto.

Dobbiamo creare lo schema.
Innanzitutto, importiamo il nostro schema corrente in MysqlWorbench usando la funzione "Database/reverse engineer". Questo ci permetterà di avere le tabelle predefinite di Sonata poiché andremo a lavorare con i media.



Con MysqlWorbench, scegliamo una relazione 1->N tra la nostra tabella "progetti" e la tabella "progetti_medias" e poi una relazione N->1 tra la tabella "progetti_medias" e la tabella "media__media".

1->N = OneToMany
N->1 = ManyToOne

La sottigliezza consiste nel mettere un id autoincrementante predefinito nella nostra tabella di collegamento e non avere le nostre chiavi esterne come chiave primaria.

Ecco cosa otteniamo:

projet_id conterrà l'id della tabella progetti e media_id conterrà l'id della tabella media__media.
Aggiungiamo un campo di ordine ma possiamo aggiungere tanti campi quanto desideriamo. Perché vedremo come gestire l'intero modulo per modificare gli elementi di progetti_medias.

In MysqlWorbench, scegliamo "Forward/reverse engineer" per inserire il nostro schema in MySQL.

Facciamo un ultimo trucco con i comandi di Symfony per generare le nostre entità e le nostre interfacce di amministrazione predefinite.

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

È magico ma incompleto. Dobbiamo correggere le relazioni nelle nostre 2 entità.
Definiremo già le proprietà di accesso in entrambe le direzioni.

Per l'entità App\Entity\Projets sarà la proprietà documenti, con gli accessori addDocument e 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;
}


Per l'entità App\Entity\ProjetsMedias sarà la proprietà progetti, con gli accessori getProjets() e setProjets().

private $projets;

public function getProjets(): ?Projets
{
    return $this->projets;
}

public function setProjets(?Projets $projets): self
{
    $this->projets = $projets;
    return $this;
}

Concentriamoci sulla configurazione della relazione Projets->ProjetsMedias perché qui sta il segreto.

/**
 * @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 proprietà targetEntity della nostra OneToMany deve puntare all'entità della nostra tabella che contiene l'elenco dei documenti e dobbiamo dargli la proprietà della nostra relazione ManyToOne di questa entità.
Quindi è \App\Entity\ProjetsMedias che ha una proprietà progetti. Gli diamo poi il nome della tabella MySQL a cui deve unirsi: projets_media. JoinColumn definisce il nome del campo nella tabella unita che conterrà le nostre chiavi primarie del progetto, quindi il valore della nostra proprietà id, che deve essere memorizzato nel projets_id di projets_media.

Poi configuriamo la nostra relazione 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;
}

La proprietà targetEntity della nostra ManyToOne deve puntare all'entità della nostra tabella che gestisce i progetti e dobbiamo dargli la proprietà della nostra relazione OneToMany di questa entità.
Quindi è Projets che ha una proprietà documenti.
Gli diamo poi il campo della nostra tabella attuale (ProjetsMedia) che memorizza la chiave primaria del progetto. Quindi id dalla tabella progetti che è memorizzato in projets_id di projets_media

Questo non è altro che il contrario di ciò che abbiamo configurato per la nostra definizione di \App\Entity\Projets::$documenti

Ultimo passo, poiché abbiamo scelto di lavorare con i media, dobbiamo configurare la nostra relazione con la libreria media di Sonata e configurare gli accessori.

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;
}

Come sopra. Iniziamo dando l'entità target, qui "App\Application\Sonata\MediaBundle\Entity\Media". Perché è l'entità che abbiamo esteso nella nostra configurazione del bundle Sonata/Media.

In sonata_media.yml abbiamo questo:

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

Sembra ovvio ma se vi imbattete in questo articolo senza avere tutte le nozioni di base, sembra saggio ricordarvelo. Potrebbe sbloccare più di una persona. 😊

Poi JoinColumn definisce come registriamo la relazione. Quindi è il campo media_id dalla nostra tabella projets_media che conterrà il campo id della nostra tabella che gestisce i media (media__media).

Ora dobbiamo solo configurare il campo media del nostro admin che abbiamo generato automaticamente sopra (ProjetsMediasAdmin.php).
Per gestire un campo media, basta utilizzare il ModelListType di Sonata. Sei libero di attivare i pulsanti opzionali di aggiunta/modifica/lista/eliminazione.

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')
    ;
}

Le relazioni sono ora complete. Possiamo verificare che tutto funzioni attivando il nostro admin di ProjetsMedia.

Nel nostro servizio, la generazione delle nostre interfacce ci ha configurato questo:

    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, ~]

Ora possiamo andare direttamente alla nostra interfaccia per vedere se funziona.

Ma ciò che ci interessa è avere questa interfaccia di gestione direttamente nella gestione dei nostri Progetti.

E per questo useremo il CollectionType di Sonata.
Aggiungiamo al nostro admin generato automaticamente, nel metodo che gestisce la configurazione del modulo di modifica, un campo documenti che avrà CollectionType come suo tipo con la sua configurazione, e soprattutto, aggiungiamo il parametro admin_code che punta al nome del nostro servizio admin di 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'
    ]
);

Incidentalmente, in \App\Admin\ProjetsMediasAdmin, rimuoviamo il nostro campo progetti poiché il valore verrà riempito automaticamente quando l'admin è nidificato nei progetti.

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')
    ;
}

E la magia avviene. Ora possiamo aggiungere/rimuovere media direttamente all'interno della nostra interfaccia.

Ma non è ancora finita. Abbiamo un campo per determinare l'ordine del nostro display. Basta configurare l'admin di ProjetsMedia per modificare il tipo del campo ordine.

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)
    ;
}

Poi configuriamo la nostra Collection in Projets, in modo che il parametro sortable punti al nostro campo corretto.

$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'
    ]
);

E infine, modifichiamo la nostra relazione Projets->ProjetsMedia per impostare l'ordine desiderato.

/**
 * @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;

La nostra collezione può ora essere ordinata semplicemente con il trascinamento.