Stap 8: Beschrijving van de gegevensstructuur

5.0 version
Maintained

Beschrijving van de gegevensstructuur

Om in PHP met een database te werken gaan we gebruik maken van Doctrine , een set libraries die ons, ontwikkelaars, helpt om met databases om te gaan:

1
$ symfony composer req orm

Dit commando installeert een paar dependencies: Doctrine DBAL (een database-abstractielaag), Doctrine ORM (een library om met de database te werken door gebruik te maken van PHP-objecten), en Doctrine Migrations.

Doctrine ORM configureren

Hoe weet Doctrine met welke database te verbinden? Doctrine’s recipe voegde een configuratiebestand toe config/packages/doctrine.yaml, dat zijn gedrag regelt. De belangrijkste instelling is de database DSN, een string die alle informatie over de verbinding bevat: gebruikersnaam, wachtwoord, host, poort, enz. Standaard probeert Doctrine deze gegevens uit de DATABASE_URL omgevingsvariabele te halen.

Conventies van Symfony-omgevingsvariabelen begrijpen

Je kan de DATABASE_URL handmatig in het .env of .env.local bestand definiëren. Dankzij het recipe van de package zie je bijvoorbeeld DATABASE_URL in jouw .env bestand. Maar omdat Docker de lokale poort voor PostgreSQL vrij kiest, is dat hinderlijk. Er is een betere manier.

In plaats van de DATABASE_URL hard te coderen in een bestand, kunnen we alle commando’s met symfony prefixen. Dit zorgt ervoor dat de Docker en/of SymfonyCloud (wanneer de tunnel open is) services gedetecteerd worden en automatisch als omgevingsvariabele ingesteld worden.

Docker Compose en SymfonyCloud werken naadloos samen met Symfony dankzij deze omgevingsvariabelen.

Bekijk alle beschikbare omgevingsvariabelen door het uitvoeren van symfony var:export:

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

Herinner je je de database servicenaam die in de Docker en SymfonyCloud configuraties wordt gebruikt? De servicenamen worden gebruikt als prefix bij het definiëren van omgevingsvariabelen zoals DATABASE_URL. Als services de Symfony naamgevingsconventies volgen, is er geen extra configuratie nodig.

Notitie

De database is niet de enige service die profiteert van de Symfony conventies. Hetzelfde geldt bijvoorbeeld voor Mailer (via de MAILER_DSN omgevingsvariabele).

De standaard DATABASE_URL waarde in .env aanpassen

We zullen het .env bestand nog steeds aanpassen om de standaard DATABASE_DSN voor het gebruik van PostgreSQL in te stellen:

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 ###

Waarom moet de informatie op twee verschillende plaatsen worden herhaald? Omdat op sommige Cloud platformen tijdens de build, de URL van de database nog onbekend kan zijn, maar Doctrine wel het database systeem moet kennen om de configuratie te kunnen opbouwen. Dus, de host, gebruikersnaam en wachtwoord doen er niet toe.

Entity classes aanmaken

Een conferentie kunnen we beschrijven aan de hand van een aantal eigenschappen:

  • De stad waar de conferentie wordt georganiseerd;
  • Het jaar van de conferentie;
  • Een internationale vlag om aan te geven of de conferentie lokaal of internationaal is (SymfonyLive vs SymfonyCon).

De Maker bundle kan ons helpen om een class (een Entity class) te genereren voor de conferentie:

1
$ symfony console make:entity Conference

Dit commando is interactief: het begeleidt je bij het toevoegen van de nodige velden. Gebruik de volgende antwoorden (de meeste zijn de standaard antwoorden, dus je kunt op de “Enter” toets drukken om ze te aanvaarden):

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

Dit is de volledige uitvoer van het commando:

 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

De Conference class is opgeslagen onder de App\Entity\ namespace.

Het commando genereerde ook een Doctrine repository class: App\Repository\ConferenceRepository .

De gegenereerde code ziet er als volgt uit (slechts een klein deel van het bestand wordt hier getoond):

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

    // ...
}

Merk op dat de class een gewone PHP class is zonder invloeden van Doctrine. Annotations worden gebruikt om metadata toe te voegen die Doctrine gebruikt om de class te kunnen koppelen aan de bijhorende databasetabel.

Doctrine heeft een id eigenschap toegevoegd om de primaire sleutel van de rij te bewaren in de tabel. Deze sleutel ( @ORM\Id() ) wordt automatisch gegenereerd ( @ORM\GeneratedValue() ) via een strategie die afhankelijk is van het gebruikte databasesysteem.

Genereer nu een Entity class voor reacties op de conferentie:

1
$ symfony console make:entity Comment

Geef de volgende antwoorden:

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

Entities aan elkaar koppelen

De twee entities, Conference en Comment, moeten aan elkaar worden gekoppeld. Een conferentie kan nul of meer reacties hebben, wat een one-to-many-relatie wordt genoemd.

Gebruik het make:entity commando opnieuw om de relatie toe te voegen aan de Conference class:

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

Notitie

Als je ? als antwoord voor het type intypt, krijg je een lijst met alle ondersteunde types:

 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

Bekijk de volledige diff van de entity class na het toevoegen van de relatie:

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

Alles wat nodig is om de relatie te beheren is nu voor je gegenereerd. Eenmaal gegenereerd, wordt dit jouw code; je bent vrij om de code aan te passen als dat nodig is.

Extra eigenschappen toevoegen

Ik realiseerde me net dat we vergeten zijn een eigenschap toe te voegen aan de Comment entity: de deelnemers willen misschien een foto van de conferentie toevoegen om hun feedback kracht bij te zetten.

Voer make:entity opnieuw uit en voeg een photoFilename eigenschap/kolom van het type string toe, maar laat null toe omdat het uploaden van een foto optioneel is:

1
$ symfony console make:entity Comment

De database migreren

Het model van het project wordt nu volledig beschreven door de twee gegenereerde classes.

Vervolgens moeten we ook nog de databasetabellen aanmaken die bij deze PHP entities horen.

Doctrine Migrations is hiervoor het beste gereedschap. Het werd al geïnstalleerd als onderdeel van de orm dependency.

Een migratie is een class die database schemawijzigingen beschrijft. Met die schemawijzigingen kan je de database van de huidige naar de nieuwe versie brengen. De schemawijzigingen worden gegenereerd op basis van de annotations die op de entity gedefinieerd zijn. De database is momenteel leeg, dus de migratie zou de creatie van twee tabellen moeten bevatten.

Laten we eens bekijken wat Doctrine genereert:

1
$ symfony console make:migration

Let op de gegenereerde bestandsnaam (ziet eruit als 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
    {
        // ...
    }
}

Bijwerken van de lokale database

Je kan nu de gegenereerde migratie uitvoeren om het lokale database schema bij te werken:

1
$ symfony console doctrine:migrations:migrate

Het lokale database-schema is nu up-to-date, klaar om gegevens te bewaren.

De productiedatabase bijwerken

De stappen die nodig zijn om de productiedatabase te migreren zijn dezelfde als die waarmee je al bekend bent: commit de wijzigingen en deploy deze.

Bij het deployen van het project brengt SymfonyCloud de code up-to-date en voert ook de databasemigraties uit (indien het doctrine:migrations:migrate commando bestaat).


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