Passo 8: Descrivere la struttura dati

5.0 version
Maintained

Descrivere la struttura dati

Per gestire il database da PHP, il progetto dipenderà da Doctrine, un insieme di librerie che aiutano gli sviluppatori a gestire i database:

1
$ symfony composer req orm

Questo comando installa alcune dipendenze: Doctrine DBAL (un livello di astrazione del database), Doctrine ORM (una libreria per manipolare il contenuto del database usando oggetti PHP), e Doctrine Migrations.

Configurare l’ORM di Doctrine

Come fa Doctrine a conoscere la connessione al database? La ricetta di Doctrine ha aggiunto un file di configurazione, config/packages/doctrine.yaml, che controlla il suo comportamento. L’impostazione principale è il DSN (Data Source Name), una stringa contenente tutte le informazioni sulla connessione: credenziali, host, porta, ecc. Per impostazione predefinita, Doctrine cerca una variabile d’ambiente DATABASE_URL.

Comprendere le convenzioni delle variabili d’ambiente di Symfony

È possibile definire la variabile DATABASE_URL manualmente nel file .env o nel file .env.local. La ricetta del pacchetto ha inserito un valore di esempio per DATABASE_URL nel file .env. Ma poiché la porta locale di PostgreSQL esposta da Docker potrebbe cambiare, tale definizione è piuttosto imprecisa. Cerchiamo una soluzione migliore.

Invece di specificare manualmente il valore di DATABASE_URL in un file, possiamo aggiungere a tutti i comandi il prefisso symfony. Questo prefisso consentirà di rilevare i servizi eseguiti da Docker e/o SymfonyCloud (quando il tunnel è aperto) e imposterà automaticamente la variabile d’ambiente.

Docker Compose e SymfonyCloud funzionano perfettamente con Symfony, grazie a queste variabili d’ambiente.

Controllare tutte le variabili d’ambiente esposte eseguendo symfony var:export:

1
$ symfony var:export
1
2
DATABASE_URL=postgres://main:[email protected]:32781/main?sslmode=disable&charset=utf8
# ...

Ricordate il nome del servizio database usato nelle configurazioni Docker e SymfonyCloud? I nomi dei servizi sono usati come prefissi per definire variabili d’ambiente come DATABASE_URL. Se i servizi sono nominati secondo le convenzioni di Symfony, non sono necessarie altre configurazioni.

Nota

I database non sono l’unico servizio che beneficia delle convenzioni di Symfony. Lo stesso vale per Mailer, per esempio (tramite la variabile d’ambiente MAILER_DSN).

Modifica del valore predefinito DATABASE_URL in .env

Modificheremo ancora il file .env, cambiando il valore predefinito di DATABASE_DSN, affinché utilizzi PostgreSQL:

1
2
3
4
5
6
7
8
9
--- a/.env
+++ b/.env
@@ -25,5 +25,5 @@ APP_SECRET=447c9fa8420eb53bbd4492194b87de8f
 # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
 # For a PostgreSQL database, use: "postgresql://db_user:[email protected]:5432/db_name?serverVersion=11&charset=utf8"
 # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
-DATABASE_URL=mysql://db_user:[email protected]:3306/db_name?serverVersion=5.7
+DATABASE_URL=postgresql://127.0.0.1:5432/db?serverVersion=11&charset=utf8
 ###< doctrine/doctrine-bundle ###

Perché le informazioni devono essere duplicate in due punti diversi? Perché su alcune piattaforme Cloud, in fase di compilazione, l’URL del database potrebbe non essere ancora noto, ma Doctrine ha bisogno di conoscere il tipo di database (PostgreSQL, MySQL, SQLite, ecc) per costruire la sua configurazione. Quindi, l’host, il nome utente e la password non hanno molta importanza.

Creazione di classi Entity

Una conferenza può essere descritta con alcune proprietà:

  • La città dove viene organizzata la conferenza;
  • L” anno della conferenza;
  • Un campo international per indicare se la conferenza è locale o internazionale (SymfonyLive vs SymfonyCon).

MakerBundle può aiutarci a generare una classe (una classe Entity) che rappresenta una conferenza:

1
$ symfony console make:entity Conference

Questo comando è interattivo: vi guiderà nel processo di aggiunta di tutti i campi necessari. Scegliere le seguenti risposte (la maggior parte di esse sono quelle predefinite, in modo da poterle scegliere premendo il tasto «Invio»):

  • city, string, 255, no;
  • year, string, 4, no;
  • isInternational, boolean, no.

Ecco l’output completo quando si esegue il comando:

 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
 created: src/Entity/Conference.php
 created: src/Repository/ConferenceRepository.php

 Entity generated! Now let's add some fields!
 You can always add more fields later manually or by re-running this command.

 New property name (press <return> to stop adding fields):
 > city

 Field type (enter ? to see all types) [string]:
 >

 Field length [255]:
 >

 Can this field be null in the database (nullable) (yes/no) [no]:
 >

 updated: src/Entity/Conference.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > year

 Field type (enter ? to see all types) [string]:
 >

 Field length [255]:
 > 4

 Can this field be null in the database (nullable) (yes/no) [no]:
 >

 updated: src/Entity/Conference.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > isInternational

 Field type (enter ? to see all types) [boolean]:
 >

 Can this field be null in the database (nullable) (yes/no) [no]:
 >

 updated: src/Entity/Conference.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 >



  Success!


 Next: When you're ready, create a migration with make:migration

La classe Conference è stata salvata sotto il namespace App\Entity\.

Il comando ha anche generato una classe repository di Doctrine: App\Repository\ConferenceRepository.

Il codice generato ha il seguente aspetto (solo una piccola parte del file viene riportata):

src/App/Entity/Conference.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
26
27
28
29
30
31
32
33
34
35
36
37
38
namespace App\Entity;

use App\Repository\ConferenceRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=ConferenceRepository::class)
 */
class Conference
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $city;

    // ...

    public function getCity(): ?string
    {
        return $this->city;
    }

    public function setCity(string $city): self
    {
        $this->city = $city;

        return $this;
    }

    // ...
}

Si noti che la classe stessa è una semplice classe PHP, senza segni di Doctrine. Le annotazioni sono usate per aggiungere metadati utili a Doctrine per mappare la classe alla relativa tabella del database.

Doctrine ha aggiunto una proprietà id per memorizzare la chiave primaria della riga nella tabella del database. Questa chiave (@ORM\Id()) viene generata automaticamente (@ORM\GeneratedValue()) tramite una strategia che dipende dal tipo di database.

Ora, generiamo una classe entity per i commenti alla conferenza:

1
$ symfony console make:entity Comment

Inserire le seguenti risposte:

  • author, string, 255, no;
  • text, text, no;
  • email, string, 255, no;
  • createdAt, datetime, no.

Collegare le entity

Le due entity, Conference e Comment, dovrebbero essere collegate tra loro. Una conferenza può avere zero o più commenti, che è detta relazione uno-a-molti.

Usare di nuovo il comando make:entity per aggiungere questa relazione alla classe Conference:

1
$ symfony console make:entity Conference
 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
 Your entity already exists! So let's add some new fields!

 New property name (press <return> to stop adding fields):
 > comments

 Field type (enter ? to see all types) [string]:
 > OneToMany

 What class should this entity be related to?:
 > Comment

 A new property will also be added to the Comment class...

 New field name inside Comment [conference]:
 >

 Is the Comment.conference property allowed to be null (nullable)? (yes/no) [yes]:
 > no

 Do you want to activate orphanRemoval on your relationship?
 A Comment is "orphaned" when it is removed from its related Conference.
 e.g. $conference->removeComment($comment)

 NOTE: If a Comment may *change* from one Conference to another, answer "no".

 Do you want to automatically delete orphaned App\Entity\Comment objects (orphanRemoval)? (yes/no) [no]:
 > yes

 updated: src/Entity/Conference.php
 updated: src/Entity/Comment.php

Nota

Se si inserisce ? come risposta al tipo, si ottengono tutti i tipi supportati:

 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
Main types
  * string
  * text
  * boolean
  * integer (or smallint, bigint)
  * float

Relationships / Associations
  * relation (a wizard will help you build the relation)
  * ManyToOne
  * OneToMany
  * ManyToMany
  * OneToOne

Array/Object Types
  * array (or simple_array)
  * json
  * object
  * binary
  * blob

Date/Time Types
  * datetime (or datetime_immutable)
  * datetimetz (or datetimetz_immutable)
  * date (or date_immutable)
  * time (or time_immutable)
  * dateinterval

Other Types
  * decimal
  * guid
  * json_array

Date un’occhiata al diff completo per le classi entity dopo aver aggiunto la relazione:

 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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
--- a/src/Entity/Comment.php
+++ b/src/Entity/Comment.php
@@ -36,6 +36,12 @@ class Comment
      */
     private $createdAt;

+    /**
+     * @ORM\ManyToOne(targetEntity=Conference::class, inversedBy="comments")
+     * @ORM\JoinColumn(nullable=false)
+     */
+    private $conference;
+
     public function getId(): ?int
     {
         return $this->id;
@@ -88,4 +94,16 @@ class Comment

         return $this;
     }
+
+    public function getConference(): ?Conference
+    {
+        return $this->conference;
+    }
+
+    public function setConference(?Conference $conference): self
+    {
+        $this->conference = $conference;
+
+        return $this;
+    }
 }
--- a/src/Entity/Conference.php
+++ b/src/Entity/Conference.php
@@ -2,6 +2,8 @@

 namespace App\Entity;

+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;

 /**
@@ -31,6 +33,16 @@ class Conference
      */
     private $isInternational;

+    /**
+     * @ORM\OneToMany(targetEntity=Comment::class, mappedBy="conference", orphanRemoval=true)
+     */
+    private $comments;
+
+    public function __construct()
+    {
+        $this->comments = new ArrayCollection();
+    }
+
     public function getId(): ?int
     {
         return $this->id;
@@ -71,4 +83,35 @@ class Conference

         return $this;
     }
+
+    /**
+     * @return Collection|Comment[]
+     */
+    public function getComments(): Collection
+    {
+        return $this->comments;
+    }
+
+    public function addComment(Comment $comment): self
+    {
+        if (!$this->comments->contains($comment)) {
+            $this->comments[] = $comment;
+            $comment->setConference($this);
+        }
+
+        return $this;
+    }
+
+    public function removeComment(Comment $comment): self
+    {
+        if ($this->comments->contains($comment)) {
+            $this->comments->removeElement($comment);
+            // set the owning side to null (unless already changed)
+            if ($comment->getConference() === $this) {
+                $comment->setConference(null);
+            }
+        }
+
+        return $this;
+    }
 }

Tutto ciò di cui si ha bisogno per gestire la relazione è stato generato. Una volta generato, il codice diventa dello sviluppatore, che è libero di personalizzarlo come preferisce.

Aggiungere altre proprietà

Ho appena realizzato che ci siamo dimenticati di aggiungere una proprietà sull’entity Comment: i partecipanti potrebbero voler allegare una foto della conferenza per illustrare il loro feedback.

Eseguire make:entity ancora una volta e aggiungere una proprietà/colonna photoFilename di tipo string, ma consentendo che accetti il valore null in modo da rendere opzionale il caricamento:

1
$ symfony console make:entity Comment

Migrazione del database

Il modello di progetto è ora completamente descritto dalle due classi generate.

Successivamente, abbiamo bisogno di creare le tabelle del database relative a queste entity PHP.

Doctrine Migrations è la soluzione perfetta per un compito del genere. È già stato installato come parte della dipendenza orm.

Una migrazione è una classe che descrive le modifiche necessarie per aggiornare lo schema di un database dal suo stato attuale a quello nuovo, definito dalle annotazioni delle entity. Poiché per ora il database è vuoto, la migrazione dovrebbe essere composta dalla sola creazione di due tabelle.

Vediamo cosa genera Doctrine:

1
$ symfony console make:migration

Si noti il nome del file generato nell’output (un nome che assomiglia a migrations/Version20191019083640.php):

migrations/Version20191019083640.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20191019083640 extends AbstractMigration
{
    public function up(Schema $schema) : void
    {
        // this up() migration is auto-generated, please modify it to your needs
        $this->addSql('CREATE SEQUENCE comment_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
        $this->addSql('CREATE SEQUENCE conference_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
        $this->addSql('CREATE TABLE comment (id INT NOT NULL, conference_id INT NOT NULL, author VARCHAR(255) NOT NULL, text TEXT NOT NULL, email VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, photo_filename VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
        $this->addSql('CREATE INDEX IDX_9474526C604B8382 ON comment (conference_id)');
        $this->addSql('CREATE TABLE conference (id INT NOT NULL, city VARCHAR(255) NOT NULL, year VARCHAR(4) NOT NULL, is_international BOOLEAN NOT NULL, PRIMARY KEY(id))');
        $this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526C604B8382 FOREIGN KEY (conference_id) REFERENCES conference (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
    }

    public function down(Schema $schema) : void
    {
        // ...
    }
}

Aggiornamento del database locale

Ora è possibile eseguire la migrazione generata per aggiornare lo schema del database locale:

1
$ symfony console doctrine:migrations:migrate

Lo schema del database locale è ora aggiornato e pronto a memorizzare dati.

Aggiornamento del database di produzione

I passi necessari per migrare il database di produzione sono gli stessi visti in precedenza: commit delle modifiche e deploy.

Quando si esegue il deploy del progetto, SymfonyCloud aggiorna il codice, ma esegue anche l’eventuale migrazione del database (verificando la presenza del comando doctrine:migrations:migrate).


  • « Previous Passo 7: Impostare il database
  • Next » Passo 9: Impostare un pannello amministrativo

This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.