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

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

Для роботи з базою даних з PHP ми будемо використовувати Doctrine, набір бібліотек, які допомагають розробникам керувати базами даних: 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 і/чи Platform.sh (коли відкрито тунель) і автоматично встановити змінну середовища.

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

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

1
$ symfony var:export
1
2
DATABASE_URL=postgres://main:main@127.0.0.1:32781/main?sslmode=disable&charset=utf8
# ...

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

Note

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

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

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

1
2
3
4
5
6
7
8
9
10
11
--- a/.env
+++ b/.env
@@ -28,7 +28,7 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
 #
 # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
 # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
-DATABASE_URL="postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8"
+DATABASE_URL="postgresql://127.0.0.1:5432/db?serverVersion=13&charset=utf8"
 ###< doctrine/doctrine-bundle ###

 ###> symfony/messenger ###

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

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

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

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

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

Тепер настав час створити сутність Conference:

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  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  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  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  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
namespace App\Entity;

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

#[ORM\Entity(repositoryClass: ConferenceRepository::class)]
class Conference
{
    #[ORM\Column(type: 'integer')]
    #[ORM\Id, ORM\GeneratedValue()]
    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_immutable, 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  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

Note

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

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
--- a/src/Entity/Comment.php
+++ b/src/Entity/Comment.php
@@ -36,6 +36,12 @@ class Comment
      */
     private $createdAt;

+    #[ORM\ManyToOne(inversedBy: 'comments')]
+    #[ORM\JoinColumn(nullable: false)]
+    private Conference $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 Version00000000000000 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

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

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

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

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

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