Il mapping delle relazioni in Doctrine è un concetto fondamentale che facilita la rappresentazione e la gestione dei dati in applicazioni Symfony. Come già affrontato, il “mapping” permette la creazione di collegamenti e associazioni tra le classi PHP, ovvero le Entity, e le corrispondenti tabelle del database. È un processo che consente di tradurre e sincronizzare gli oggetti dell’applicazione con i dati memorizzati all’interno di un database.
Relazioni tra tabelle in Symfony
Nel contesto dei database, le relazioni sono connessioni logiche stabilite tra diverse tabelle. Queste relazioni sono fondamentali per organizzare e strutturare i dati in modo efficiente, garantendo integrità e coerenza dei dati. Le relazioni tra le tabelle sono categorizzate principalmente in tre tipi:
- Uno-a-Uno (1:1): In questo tipo di relazione, un record in una tabella è associato a un solo record in un’altra tabella.
- Uno-a-Molti (1:N): Qui, un record in una tabella può essere associato a più record in un’altra tabella.
- Molti-a-Molti (M:N): In questa relazione, più record in una tabella possono essere associati a più record in un’altra tabella.
Di seguito, un esempio di relazioni tra tabelle nell’ambito di un tipo sistema di e-commerce:
- Esempio 1:1: un ordine ha una spedizione associata ed una spedizione fa riferimento ad un solo ordine.
- Esempio 1:N: una categoria può contenere più prodotti ma un prodotto può appartenere ad una sola categoria.
- Esempio M:N: un ordine può contenere più prodotti e un prodotto può essere aggiunto a più ordini.
Nelle sezioni successive andremo a sviluppare gli esempi elencati precedentemente all’interno del nostro progetto Symfony di test. Pronto? Cominciamo!
Mapping 1:1 in Symfony: relazione Uno-a-Uno
Per creare le entità Ordine e Spedizione utilizziamo, come al solito, il comando make.
In Ordine aggiungiamo le proprietà stato, per tracciare lo stato dell’ordine, e prezzoTotale. In Spedizione, invece, prevediamo le proprietà corriere, trackingNumber e dataConsegnaStimata. Il passaggio successivo è quello di mettere in relazione le due entità. Accediamo in modifica ad Ordine, sempre con l’aiuto del comando make, e aggiungiamo un campo di tipo relation per mappare la relazione con Spedizione.
Come si nota, il comando richiede una serie di informazioni per creare correttamente la relazione desiderata tra le entità:
- per prima cosa viene chiesto quale è l’entità con la quale relazionare Ordine. Digitiamo “Spedizione”
- Il passo successivo è la selezione della tipologia di relazione tra le entità. Symfony ci propone le varie opzioni in una tabella riepilogativa spiegando anche quali sono le caratteristiche di ognuna. Nel nostro caso digitiamo “OneToOne”.
- La nuova proprietà spedizione in Ordine può essere null in quanto, se l’ordine è ancora in fase di validazione, la spedizione non sarà stata creata ed associata.
- Possiamo scegliere se aggiungere o meno in Spedizione una proprietà per accedere alla relativa istanza di Ordine. Abbiamo digitato “yes”. Successivamente, ci viene richiesto il nome di tale proprietà che chiameremo ordine.
Conclusa l’esecuzione di questo comando, avremo le entità Ordine e Spedizione aggiornate e collegate da una relazione di tipo 1 a 1. Di seguito, il codice sorgente delle classi Ordine e Spedizione (sono stati rimossi per brevità proprietà e metodi non inerenti la relazione creata).
/src/Entity/Ordine.php <?php // imports . . . #[ORM\Entity(repositoryClass: OrdineRepository::class)] class Ordine { // proprietà . . . #[ORM\OneToOne(inversedBy: 'ordine', cascade: ['persist', 'remove'])] private ?Spedizione $spedizione = null; // getter e setter . . . public function getSpedizione(): ?Spedizione { return $this->spedizione; } public function setSpedizione(?Spedizione $spedizione): static { $this->spedizione = $spedizione; return $this; } } /src/Entity/Spedizione.php <?php // imports . . . #[ORM\Entity(repositoryClass: SpedizioneRepository::class)] class Spedizione { // proprietà . . . #[ORM\OneToOne(mappedBy: 'spedizione', cascade: ['persist', 'remove'])] private ?Ordine $ordine = null; // getter e setter . . . public function getOrdine(): ?Ordine { return $this->ordine; } public function setOrdine(?Ordine $ordine): static { // unset the owning side of the relation if necessary if ($ordine === null && $this->ordine !== null) { $this->ordine->setSpedizione(null); } // set the owning side of the relation if necessary if ($ordine !== null && $ordine->getSpedizione() !== $this) { $ordine->setSpedizione($this); } $this->ordine = $ordine; return $this; } }
Notiamo l’utilizzo del PHP attribute ORM\OneToOne in entrambe le classi:
// Ordine #[ORM\OneToOne(inversedBy: 'ordine', cascade: ['persist', 'remove'])] private ?Spedizione $spedizione = null; // Spedizione #[ORM\OneToOne(mappedBy: 'spedizione', cascade: ['persist', 'remove'])] private ?Ordine $ordine = null;
Tale sintassi è fondamentale per definire il mapping tra le Entity. All’interno di esse notiamo le opzioni inversedBy e mappedBy. In Doctrine, sono utilizzate per specificare e configurare le relazioni bidirezionali tra le entità. Sono essenziali per definire chiaramente la proprietà che mappa e gestisce la relazione tra le due parti coinvolte.
mappedBy viene utilizzato nella classe dell’entità che è il lato “inverso” della relazione, indica che la relazione è già stata mappata dall’altra entità. L’entità con mappedBy non è responsabile per la gestione della relazione ovvero non viene utilizzata per fare modifiche dirette alla relazione, come l’aggiunta o la rimozione di elementi;
inversedBy viene utilizzato nella classe dell’entità che è il lato “proprietario” della relazione. L’entità con inversedBy è responsabile per la gestione della relazione. Le modifiche alla relazione (come l’aggiunta o la rimozione di elementi) devono essere effettuate da questo lato per essere riconosciute e applicate anche al database.
Notiamo, infine, la presenza dell’opzione cascade. Essa è utile per definire come le operazioni effettuate su un’entità debbano riflettersi sulle entità correlate.
Mapping 1:N in Symfony: relazione Uno-a-Molti
Per implementare una relazione 1:N consideriamo l’esempio Categoria-Prodotto:
Ogni prodotto appartiene ad una categoria specifica. A quella stessa categoria, però, possono afferire anche altri prodotti.
Utilizziamo il comando make per creare la Entity Categoria e generare la relazione.
Abbiamo scelto una relazione “OneToMany” in quanto, come da schermata: ogni Categoria può essere relazionata con più oggetti di tipo Prodotto. Ogni Prodotto può avere una sola Categoria.
Il comando andrà ad aggiungere anche una nuova proprietà in Prodotto per gestire la Categoria relazionata ad una specifica istanza.
Lato Entity Categoria, è stata aggiunta in automatico la proprietà prodotti che definisce la relazione con una Collection di Prodotto:
#[ORM\OneToMany(mappedBy: 'categoria', targetEntity: Prodotto::class)] private Collection $prodotti;
Dal lato di Prodotto ritroviamo la mappatura inversa:
#[ORM\ManyToOne(inversedBy: 'prodotti')] private ?Categoria $categoria = null;
Mapping M:N in Symfony: relazione Molti-a-Molti
Ora è il momento di implementare l’ultimo esempio, quello relativo alle entità Prodotto e Ordine legate da una relazione di molti a molti (M:N).
Con il comando make andiamo a richiedere le modifiche alla classe Ordine. Aggiungiamo un nuovo campo chiamato prodotti di tipo relation.
La relazione scelta è “ManyToMany”.
All’interno di Ordine è stato aggiunto in automatico il mapping della relazione:
#[ORM\ManyToMany(targetEntity: Prodotto::class, inversedBy: 'ordini')] private Collection $prodotti;
e i metodi per la gestione dei prodotti associati ad un ordine specifico:
/** * @return Collection<int, Prodotto> */ public function getProdotti(): Collection { return $this->prodotti; } public function addProdotti(Prodotto $prodotti): static { if (!$this->prodotti->contains($prodotti)) { $this->prodotti->add($prodotti); } return $this; } public function removeProdotti(Prodotto $prodotti): static { $this->prodotti->removeElement($prodotti); return $this; }
Stessa cosa è stata fatta dal lato dell’entità Prodotto che avrà come targetEntity la classe Ordine.
Tramite la relazione appena creata, attraverso un’istanza di Ordine è possibile accedere a tutti i dettagli dei prodotti associati direttamente dall’oggetto stesso attraverso il metodo getProdotti().