Passo 8: Descrevendo a Estrutura de Dados

5.0 version
Maintained

Descrevendo a Estrutura de Dados

Para lidar com o banco de dados a partir do PHP, vamos depender do Doctrine, um conjunto de bibliotecas que ajudam os desenvolvedores a gerenciar bancos de dados:

1
$ symfony composer req orm

Este comando instala algumas dependências: Doctrine DBAL (uma camada de abstração de banco de dados), Doctrine ORM (uma biblioteca para manipular o conteúdo do nosso banco de dados usando objetos PHP) e Doctrine Migrations.

Configurando o Doctrine ORM

Como o Doctrine conhece a conexão com o banco de dados? A receita do Doctrine adicionou um arquivo de configuração, config/packages/doctrine.yaml, que controla seu comportamento. A principal configuração é o DSN do banco de dados, uma string contendo todas as informações sobre a conexão: credenciais, host, porta, etc. Por padrão, o Doctrine procura uma variável de ambiente DATABASE_URL.

Entendendo as Convenções de Variável de Ambiente do Symfony

Você pode definir DATABASE_URL manualmente no arquivo .env ou .env.local. Na verdade, graças à receita do pacote, você verá um exemplo de DATABASE_URL em seu arquivo .env. Mas como a porta local para o PostgreSQL exposta pelo Docker pode mudar, isso é bastante complicado. Há uma maneira melhor.

Em vez de codificar DATABASE_URL em um arquivo, podemos prefixar todos os comandos com symfony. Isso irá detectar os serviços executados pelo Docker e/ou SymfonyCloud (quando o túnel estiver aberto) e definir a variável de ambiente automaticamente.

O Docker Compose e a SymfonyCloud funcionam perfeitamente com o Symfony graças a essas variáveis de ambiente.

Verifique todas as variáveis de ambiente expostas executando symfony var:export:

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

Lembra-se do nome de serviço database usado nas configurações do Docker e da SymfonyCloud? Os nomes de serviços são usados como prefixos para definir variáveis de ambiente como DATABASE_URL. Se seus serviços forem nomeados de acordo com as convenções do Symfony, nenhuma outra configuração será necessária.

Nota

Os bancos de dados não são os únicos serviços que se beneficiam das convenções do Symfony. O mesmo se aplica ao Mailer, por exemplo (através da variável de ambiente MAILER_DSN).

Alterando o Valor Padrão de DATABASE_URL em .env

Ainda iremos mudar o arquivo .env para configurar o DATABASE_DSN padrão para usar o 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 ###

Por que as informações precisam ser duplicadas em dois lugares diferentes? Porque em algumas plataformas em nuvem, na hora de fazer o build, a URL do banco de dados pode ainda não ser conhecida, mas o Doctrine precisa saber qual o mecanismo do banco de dados para construir sua configuração. Então, o host, o nome de usuário e a senha não importam.

Criando Classes de Entidade

Uma conferência pode ser descrita com algumas propriedades:

  • A cidade onde a conferência é organizada;
  • O ano da conferência;
  • Uma flag internacional para indicar se a conferência é local ou internacional (SymfonyLive vs. SymfonyCon).

O bundle Maker pode nos ajudar a gerar uma classe (uma classe de Entidade) que representa uma conferência:

1
$ symfony console make:entity Conference

Este comando é interativo: ele irá guiá-lo através do processo de adicionar todos os campos que você precisa. Use as seguintes respostas (a maioria delas são as padrões, assim você pode pressionar a tecla “Enter” para usá-las):

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

Aqui está a saída completa ao executar o 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

A classe Conference foi armazenada sob o namespace App\Entity\.

O comando também gerou uma classe de repositório do Doctrine: App\Repository\ConferenceRepository.

O código gerado se parece com o seguinte (apenas uma pequena parte do arquivo está replicada aqui):

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

    // ...
}

Note que a classe em si é uma classe PHP simples sem sinais do Doctrine. As annotations são usadas para adicionar metadados úteis para que o Doctrine mapeie a classe para sua respectiva tabela de banco de dados.

O Doctrine adicionou uma propriedade id para armazenar a chave primária do registro na tabela de banco de dados. Essa chave (@ORM\Id()) é gerada automaticamente (@ORM\GeneratedValue()) por meio de uma estratégia que depende do mecanismo de banco de dados.

Agora, gere uma classe de Entidade para comentários da conferência:

1
$ symfony console make:entity Comment

Digite as seguintes respostas:

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

Relacionando Entidades

As duas entidades, Conference e Comment, devem ser interligadas. Uma conferência pode ter zero ou mais comentários, o que é chamado de relacionamento um-para-muitos.

Use o comando make:entity novamente para adicionar esse relacionamento à 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 você digitar ? como uma resposta para o tipo, você obterá todos os tipos suportados:

 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

Dê uma olhada no diff completo das classes de entidade depois de adicionar o relacionamento:

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

Tudo o que você precisa para gerenciar o relacionamento foi gerado para você. Uma vez gerado, o código torna-se seu; sinta-se à vontade para personalizá-lo da maneira que quiser.

Adicionando mais Propriedades

Acabei de perceber que nos esquecemos de adicionar uma propriedade na entidade Comment: os participantes podem querer anexar uma foto da conferência para ilustrar seus comentários.

Execute make:entity mais uma vez e adicione uma propriedade/coluna chamada photoFilename do tipo string, mas permita que ela seja null, já que o upload de uma foto é opcional:

1
$ symfony console make:entity Comment

Migrando o Banco de Dados

O modelo do projeto está agora totalmente descrito pelas duas classes geradas.

Em seguida, precisamos criar as tabelas de banco de dados relacionadas a essas entidades PHP.

Doctrine Migrations é a combinação perfeita para essa tarefa. Ele já foi instalado como parte da dependência orm.

Uma migração é uma classe que descreve as alterações necessárias para atualizar um esquema de banco de dados de seu estado atual para o novo estado definido pelas annotations da entidade. Como o banco de dados está vazio por enquanto, a migração deve consistir na criação de duas tabelas.

Vamos ver o que o Doctrine gera:

1
$ symfony console make:migration

Observe o nome do arquivo gerado na saída (um nome que se parece com 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
    {
        // ...
    }
}

Atualizando o Banco de Dados Local

Agora você pode executar a migração gerada para atualizar o esquema do banco de dados local:

1
$ symfony console doctrine:migrations:migrate

O esquema do banco de dados local agora está atualizado, pronto para armazenar alguns dados.

Atualizando o Banco de Dados de Produção

Os passos necessários para migrar o banco de dados de produção são os mesmos que você já conhece: fazer o commit das alterações e implantar.

Ao implantar o projeto, a SymfonyCloud atualiza o código, mas também executa a migração do banco de dados, se houver (ela detecta se o comando doctrine:migrations:migrate existe).


  • « Previous Passo 7: Configurando um Banco de Dados
  • Next » Passo 9: Configurando um Painel Administrativo

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