Крок 8: Опис структури даних

5.2 version
Maintained

Опис структури даних

Для роботи з базою даних в PHP ми будемо використовувати Doctrine, — набір бібліотек, які допомагають розробникам працювати з базами даних:

1
$ symfony composer req "orm:^2"

Ця команда встановлює кілька залежностей: Doctrine DBAL (рівень абстракції бази даних), Doctrine ORM (бібліотека для управління вмістом нашої бази даних за допомогою об’єктів PHP) та Doctrine Migrations.

Налаштування Doctrine ORM

Як Doctrine підключається до бази даних? Рецепт Doctrine додав файл конфігурації config/packages/doctrine.yaml, який містить параметри для підключення. Основним параметром є DSN бази даних — рядок, що містить всю інформацію про з’єднання: облікові дані, адреса, порт тощо. За замовчуванням Doctrine шукає змінну середовища DATABASE_URL.

Майже всі встановлені пакети мають конфігурацію в каталозі config/packages/. Значення за замовчуванням здебільшого були ретельно підібрані для роботи в більшості застосунків.

Домовленості про іменування змінних середовища в Symfony

Ви можете визначити параметр DATABASE_URL вручну у файлі .env або .env.local. Насправді, завдяки рецепту пакета, ви побачите приклад DATABASE_URL у вашому файлі .env. Але він досить громіздкий, оскільки локальний порт PostgreSQL, що надає нам Docker, може змінюватися. Є кращий спосіб.

Замість того щоб жорстко задати значення DATABASE_URL у файлі, ми можемо додати префікс symfony до всіх команд. Це дозволить виявити сервіси запущені у Docker та/або SymfonyCloud (коли відкрито тунель) і автоматично встановити змінну середовища.

Docker Compose й SymfonyCloud легко працюють із Symfony завдяки цим змінним середовища.

Перевірте всі доступні змінні середовища, виконавши команду symfony var:export:

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

Пам’ятаєте ім’я сервісу database, що використовується у конфігураціях Docker та SymfonyCloud? Імена служб використовуються як префікси для визначення змінних середовища, таких як DATABASE_URL. Якщо ваші сервіси іменуються відповідно до домовленостей Symfony — додаткові налаштування не потрібні.

Примітка

База даних — не єдиний сервіс, що використовує домовленості Symfony. Це ж стосується, наприклад, Mailer (через змінну середовища MAILER_DSN).

Зміна значення DATABASE_URL за замовчуванням у .env

Ми все одно змінимо файл .env для налаштування значення DATABASE_URL за замовчуванням, щоб використовувати PostgreSQL:

1
2
3
4
5
6
7
8
9
--- a/.env
+++ b/.env
@@ -24,5 +24,5 @@ APP_SECRET=ce2ae8138936039d22afb20f4596fe97
 #
 # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
 # DATABASE_URL="mysql://db_user:[email protected]:3306/db_name?serverVersion=5.7"
-DATABASE_URL="postgresql://db_user:[email protected]:5432/db_name?serverVersion=13&charset=utf8"
+DATABASE_URL="postgresql://127.0.0.1:5432/db?serverVersion=13&charset=utf8"
 ###< doctrine/doctrine-bundle ###

Чому інформація має дублюватися у двох різних місцях? Тому, що на деяких хмарних платформах, під час збірки, URL-адреса бази даних може бути ще невідома, але Doctrine потрібно знати про систему керування базами даних, щоб створити свою конфігурацію. Таким чином, адреса, ім’я користувача й пароль, насправді, не мають значення.

Створення класів сутностей

Конференцію можна описати кількома властивостями:

  • Місто, де організована конференція;
  • Рік проведення конференції;
  • Прапорець international, що вказує, чи є конференція локальною або міжнародною (SymfonyLive або SymfonyCon).

Бандл Maker може допомогти нам згенерувати сутність (клас сутності), який являє собою конференцію:

1
$ symfony console make:entity Conference

Ця команда є інтерактивною: вона допоможе вам в процесі додавання всіх необхідних властивостей. Використовуйте наступні відповіді (більшість з них встановлено за замовчуванням, тому ви можете просто натискати клавішу «Enter», щоб застосувати їх):

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

Ось повний вивід при виконанні команди:

 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

Клас Conference знаходиться у просторі імен App\Entity\.

Команда також згенерувала клас репозиторію Doctrine: App\Repository\ConferenceRepository.

Згенерований код виглядає наступним чином (тут наводиться тільки невелика частина файлу):

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

    // ...
}

Зверніть увагу, що сам клас є простим класом PHP, не пов’язаним з Doctrine. Для додавання метаданих використовуються анотації, що дозволяють Doctrine зв’язувати клас сутності з відповідною таблицею в базі даних.

Doctrine додала властивість id, щоб зберігати первинний ключ рядка в таблиці бази даних. Цей ключ (@ORM\Id()) генерується автоматично (@ORM\GeneratedValue()) за допомогою стратегії, яка залежить від системи керування базою даних.

Тепер згенеруйте клас сутності для коментарів конференції:

1
$ symfony console make:entity Comment

Введіть наступні відповіді:

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

Зв’язування сутностей

Дві сутності, конференція та коментар, мають бути зв’язаними між собою. Конференція може мати нуль або більше коментарів, це називається зв’язком один до багатьох.

Використовуйте команду make:entity ще раз, щоб додати цей зв’язок у клас 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

Примітка

Якщо ви введете ?, у якості відповіді на питання про тип даних, то отримаєте список всіх підтримуваних типів:

 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

Погляньте на повну різницю для класів сутностей після додавання зв’язку:

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

Все, що вам потрібно для управління зв’язком, було згенеровано для вас. Після генерування код стає вашим; сміливо налаштовуйте його так, як вам хочеться.

Додавання додаткових властивостей

Я щойно зрозумів, що ми забули додати одну властивість до сутності коментаря: відвідувачі, можливо, захочуть долучити фото конференції, щоб проілюструвати свої враження.

Виконайте команду make:entity ще раз та додайте властивість/стовпчик photoFilename з типом string, але дозвольте йому мати значення null, оскільки процес завантаження фото є необов’язковим:

1
$ symfony console make:entity Comment

Міграція бази даних

Модель проекту тепер повністю описана двома згенерованими класами.

Далі нам потрібно створити таблиці бази даних, пов’язані з цими сутностями PHP.

Doctrine Migrations ідеально підходить для такого завдання. Цей пакет вже встановлено у вигляді залежності для orm.

Міграція — це клас, що описує зміни, необхідні для оновлення схеми бази даних з її поточного стану до нового, визначеного в анотаціях сутностей. Оскільки база даних поки що порожня, міграція має складатися зі створення двох нових таблиць.

Подивімося, що генерує Doctrine:

1
$ symfony console make:migration

Зверніть увагу на згенероване ім’я файлу у виводі (ім’я, схоже на 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
    {
        // ...
    }
}

Оновлення локальної бази даних

Тепер можна запустити згенеровану міграцію, для оновлення схеми локальної бази даних:

1
$ symfony console doctrine:migrations:migrate

Схема локальної бази даних тепер оновлена і готова до зберігання наших даних.

Оновлення бази даних у продакшн

Кроки, що необхідні для міграції бази даних у продакшн ті самі, з якими ви вже знайомі: фіксація змін і розгортання.

Під час розгортання проекту, SymfonyCloud оновлює код, але також виконує міграцію бази даних, якщо така є (він виявляє, чи існує команда doctrine:migrations:migrate).


  • « Previous Крок 7: Налаштування бази даних
  • Next » Крок 9: Налаштування панелі керування

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