Schritt 8: Die Datenstruktur beschreiben

5.0 version
Maintained

Die Datenstruktur beschreiben

Um in PHP mit der Datenbank umzugehen, werden wir Doctrine verwenden, eine Reihe von Bibliotheken, die Entwickler*innen helfen, Datenbanken zu verwalten:

1
$ symfony composer req orm

Dieser Befehl installiert einige Dependencies: Doctrine DBAL (eine Datenbankabstraktionsschicht), Doctrine ORM (eine Bibliothek zur Manipulation unserer Datenbankinhalte mit Hilfe von PHP-Objekten) und Doctrine Migrations.

Doctrine ORM konfigurieren

Woher kennt Doctrine die Datenbankverbindung? Das Doctrine-Recipe hat eine Konfigurationsdatei hinzugefügt, config/packages/doctrine.yaml, die das Verhalten steuert. Die wichtigste Einstellung ist die Datenbank-DSN, eine Zeichenkette, die alle Informationen über die Verbindung enthält: Anmeldeinformationen, Host, Port, etc. Standardmäßig sucht Doctrine nach der Environment-Variable DATABASE_URL.

Konventionen für Symfony-Environment-Variablen verstehen

Du kannst DATABASE_URL manuell in der .env- oder .env.local-Datei definieren. Dank des Recipes des Paketes siehst Du sogar eine beispielhafte DATABASE_URL in Deiner .env-Datei. Aber da sich der lokale Port auf PostgreSQL, der von Docker festgelegt wird, ändern kann, ist dieser Weg recht umständlich. Es gibt einen besseren Weg.

Anstatt in einer Datei fest DATABASE_URL einzusetzen, können wir alle Befehle mit symfony prefixen. Dadurch werden Dienste erkannt, die von Docker und/oder SymfonyCloud ausgeführt werden (wenn der Tunnel geöffnet ist) und die Environment-Variable wird automatisch gesetzt.

Docker Compose und SymfonyCloud arbeiten dank dieser Environment-Variablen nahtlos mit Symfony zusammen.

Du überprüfst alle exponierten Environment-Variablen, indem Du symfony var:export ausführst:

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

Erinnerst Du dich an den database-Servicenamen, der in den Konfigurationen von Docker und SymfonyCloud verwendet wird? Die Servicenamen werden als Präfixe für Environment-Variablen wie DATABASE_URL verwendet. Wenn Deine Services nach den Symfony-Konventionen benannt sind, ist keine weitere Konfiguration erforderlich.

Bemerkung

Datenbanken sind nicht der einzige Service, der von den Symfony-Konventionen profitiert. Das Gleiche gilt z. B. für Mailer (über die Environment-Variable MAILER_DSN).

Den Standardwert DATABASE_URL in .env ändern

Wir werden die .env-Datei dennoch ändern, um den Standard-DATABASE_DSN für die Verwendung von PostgreSQL festzulegen:

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

Warum müssen die Informationen an zwei verschiedenen Stellen dupliziert werden? Da auf einigen Cloud-Plattformen zum Zeitpunkt des Builds die Datenbank-URL möglicherweise noch nicht bekannt ist, muss Doctrine die Engine der Datenbank kennen, um ihre Konfiguration zu erstellen. Daher sind der Host und die Zugangsdaten nicht wirklich wichtig.

Entity-Klassen anlegen

Eine Konferenz kann mit einigen wenigen Eigenschaften beschrieben werden:

  • Die Stadt, in der die Konferenz organisiert wird;
  • Das Jahr der Konferenz;
  • Ein international-Flag, die angibt, ob die Konferenz lokal oder international ist (SymfonyLive vs. SymfonyCon).

Das Maker-Bundle kann uns helfen, eine Klasse (eine Entity-Klasse) zu generieren, die eine Konferenz repräsentiert:

1
$ symfony console make:entity Conference

Dieser Befehl ist interaktiv: Er führt Dich durch den Prozess des Hinzufügens aller benötigten Felder. Verwende die folgenden Antworten (die meisten davon sind die Standardwerte, Du kannst die Taste „Enter“ drücken, um sie zu verwenden):

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

Das ist die vollständige Ausgabe des Befehls:

 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

Die Conference-Klasse wurde unter dem App\Entity\-Namespace abgelegt.

Der Befehl erzeugte auch eine Doctrine Repository-Klasse: App\Repository\ConferenceRepository.

Der generierte Code sieht wie folgt aus (nur ein kleiner Teil der Datei wird hier gezeigt):

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

    // ...
}

Beachte, dass die Klasse selbst eine einfache PHP-Klasse ohne Anzeichen von Doctrine ist. Mittels Annotations werden Metadaten hinzugefügt, die Doctrine verwendet, um die Klasse der zugehörigen Datenbanktabelle zuzuordnen.

Doctrine hat ein id-Property/Spalte hinzugefügt, um den Primärschlüssel der Zeile in der Datenbanktabelle zu speichern. Dieser Schlüssel (@ORM\Id()) wird abhängig vom verwendeten Datenbanksystem automatisch generiert (@ORM\GeneratedValue()).

Erzeuge nun eine Entity-Klasse für Konferenzkommentare:

1
$ symfony console make:entity Comment

Gebe die folgenden Antworten ein:

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

Entities miteinander verknüpfen

Die beiden Entities, Conference und Comment, sollten miteinander verbunden werden. Eine Konferenz kann null oder mehr Kommentare haben, was als One-to-Many-Beziehung bezeichnet wird.

Verwende erneut den make:entity-Befehl, um diese Beziehung zur Conference-Klasse hinzuzufügen:

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

Bemerkung

Wenn Du als Antwort auf den Typ ? eingibst, erhältst Du alle unterstützten Typen:

 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

Wirf einen Blick auf das vollständige Diff für die Entity-Klassen, nachdem Du die Beziehung (Relation) hinzugefügt hast:

 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, was Du für die Verwaltung von relations benötigst, wurde für Dich generiert. Sobald der Code generiert ist, gehört er Dir; zöger nicht, ihn nach Deinen Wünschen anzupassen.

Weitere Properties (Spalten) hinzufügen

Mir ist gerade aufgefallen, dass wir vergessen haben, ein Property zur Comment-Entity hinzuzufügen: Die Teilnehmer*innen möchten vielleicht ein Foto der Konferenz anhängen, um ihr Feedback zu veranschaulichen.

Führe make:entity noch einmal aus und füge ein photoFilename Property/Spalte vom Typ string hinzu, aber lass es null sein, da das Hochladen eines Fotos optional ist:

1
$ symfony console make:entity Comment

Die Datenbank migrieren

Das Projektmodell wird nun durch die beiden generierten Klassen vollständig beschrieben.

Als nächstes müssen wir die Datenbanktabellen erstellen, die sich auf diese PHP-Entitys beziehen.

Doctrine Migrations ist die perfekte Ergänzung für eine solche Aufgabe. Es wurde bereits als Teil der orm-Dependency installiert.

Eine Migration ist eine Klasse, welche die Änderungen beschreibt, die erforderlich sind, um ein Datenbankschema von seinem aktuellen Zustand auf den neuen Zustand, der durch die Annotations in den Entities definiert ist, zu aktualisieren. Da die Datenbank vorerst leer ist, sollte die Migration aus zwei Einträgen bestehen.

Mal sehen, was Doctrine erzeugt:

1
$ symfony console make:migration

Beachte den generierten Dateinamen in der Ausgabe (ein Name, der so aussieht 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
    {
        // ...
    }
}

Die lokale Datenbank aktualisieren

Du kannst nun die generierte Migration ausführen, um das lokale Datenbankschema zu aktualisieren:

1
$ symfony console doctrine:migrations:migrate

Das lokale Datenbankschema ist nun auf dem aktuellen Stand und bereit, einige Daten zu speichern.

Die Datenbank der Produktivumgebung aktualisieren

Die Schritte, die für die Migration der Datenbank für die Produktivumgebung erforderlich sind, sind die gleichen wie die, mit denen Du bereits vertraut bist: Committe die Änderungen und deploye.

Beim Deployment des Projekts aktualisiert SymfonyCloud den Code, führt aber auch die Datenbankmigration durch, falls vorhanden (SymfonyCloud erkennt, ob der doctrine:migrations:migrate-Befehl existiert).


  • « Previous Schritt 7: Eine Datenbank einrichten
  • Next » Schritt 9: Ein Admin-Backend einrichten

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