Persistindo dados em PHP com o Doctrine ORM
Uma das questões que sempre vão surgir no desenvolvimento é a persistência de dados, e existem muitas tecnologias e ferramentas para resolver este problema. Na questão do mapeamento objeto relacional em particular também existem diversas alternativas, vamos ver hoje uma bem interessante o Doctrine ORM .
O Doctrine ORM é uma ferramenta mapeamento objeto relacional do conjunto de projetos Doctrine, ele utiliza o padrão Data Mapper e pode ser usado desde pequenos projetos até grandes aplicações, pode também facilmente ser utilizando em conjunto com frameworks(eu estou utilizando em alguns projetos com slim4), Suportando vários bancos como MySQL, PostgreSQL, SQL Server, Oracle, SQLite.
Para começar vamos adicionar o Doctrine nas dependências do composer.
composer require doctrine/orm
Mapeamento de Entidades Doctrine
Para o Doctrine, cada objeto que queremos persistir no banco de dados é chamado de Entidade(Entity), e estas entidades devem ser mapeadas. Podemos realizar este mapeamento de duas formas, através de anotações na própria classe do objeto ou através de arquivos XML.
Vamos ver um exemplo de classe mapeada usando Anotações.
<?php
declare(strict_types=1);
namespace BotecoDigital;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="posts")
*/
class Post
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
private $id;
/**
* @ORM\Column(type="string")
*/
private $titulo;
/**
* @ORM\Column(type="text")
*/
private $texto;
/**
* @ORM\Column(type="datetime")
*/
private $dataPublicacao;
public function setId(int $id): void
{
$this->id = $id;
}
public function getId(): int
{
return $this->id;
}
public function setTitulo(string $titulo): void
{
$this->titulo = $titulo;
}
public function getTitulo(): string
{
return $this->titulo;
}
public function setTexto(string $texto): void
{
$this->texto = $texto;
}
public function getTexto(): string
{
return $this->texto;
}
public function setDataPublicacao(DateTime $dataPublicacao)
{
$this->dataPublicacao = $dataPublicacao;
}
public function getDataPublicacao(): DateTime
{
return $this->dataPublicacao;
}
}
A classe é marcada com a anotação @ORM\Entity
para informar para o Doctrine que ela é uma classe a ser mapeada. Também anotamos a classe com @ORM\Table
passando como argumento name="posts"
, isso informa que o nome da tabela no banco de dados será posts
.
Com a classe marcada, vamos aos atributos começando com o $id
, este será nossa chave primária, então marcamos ele com @ORM\Id
, depois vamos para a definição da coluna no banco de dados, isto é feito através da anotação @ORM\Column
que recebe vários argumentos(veremos mais mais adiante), no caso definimos o tipo de dado da coluna sendo inteiro através do valor type="integer"
. E finalmente marcamos que o valor desta coluna será auto incrementado através da anotação @ORM\GeneratedValue
.
Para o atributo $titulo
definimos para a coluna para @ORM\Column(type="string")
que irá definir, no mysql no caso, uma coluna VARCHAR(255). Paro o atributo $titulo
definimos @ORM\Column(type="text")
para um texto longo e por fim para o atributo $dataPublicacao
para @ORM\Column(type="datetime")
o que irá definir este atributo com o tipo PHP de DateTime
.
Para a anotação de definição de coluna o Doctrine aceita os seguintes valores:
type
: (opcional, valor padrão ‘string’) Mapeia o tipo para o uso na coluna.name
: (opcional , valor padrão ‘<nome do atributo>’) O nome da coluna no banco de dados.length
: (opcional, valor padrão 255) O tamanho da coluna no banco de dados. (Aplica-se apenas se uma coluna com valor de string for usada).unique
: (opcional, valor padrão false) Se o valor da coluna é único.nullable
: (opcional, valor padrão false) Se a coluna no banco de dados é nullable.precision
: (opcional) Quantidade máxima de dígitos de uma coluna decimal.scale
: (opcional, valor padrão 0) Número de dígitos da parte decimal do número( obs.: conta como dígitos máximos do precision).
* Os valores de argumento de anotação são separados por ” , “, por exemplo @ORM\Column(type="decimal", precision=10, scale=2)
, Neste exemplo irá mapear para o tipo SQL DECIMAL(10,2) .
O Doctrine vários tipos para o argumento type
da anotação @ORM\Column
, segue um lista de alguns tipos:
string
: O tipo mapeia uma SQL VARCHAR para uma string PHP.integer
: O tipo mapeia um SQL INT para um integer PHP.smallint
: O tipo mapeia um um SQL SMALLINT para um integer PHP.bigint
: O tipo mapeia um SQL BIGINT para uma string PHP.boolean
: O tipo mapeia um SQL boolean ou equivalente(TINYINT) para um boolean PHP.decimal
: O tipo mapeia um SQL DECIMAL para uma string PHP.date
: O tipo mapeia um SQL DATETIME para um objeto DateTime PHP.time
: O tipo mapeia um SQL TIME para um objeto DateTime PHP.datetime
: O tipo mapeia um SQL DATETIME/TIMESTAMP para um objeto DateTime PHP.text
: O tipo mapeia um SQL CLOB para uma string PHP.float
: O tipo mapeia um SQL Float (Double Precision) para um double PHP.
Embora seja mais usual o mapeamento através de anotações, o Doctrine permite o mapeamento através de XML, para isso deve criar um arquivo com o nome da classe(namespace
incluso, barra substituídas por “.”) e com a extensão .dcm.xml
. No mapeamento da nossa classe de exemplo o nome do arquivo ficaria BotecoDigital.Post.dcm.xml
<doctrine-mapping>
<entity name="BotecoDigital\Post" table="posts">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<field name="titulo" type="string" />
<field name="texto" type="text" />
<field name="dataPublicacao" type="datetime" />
</entity>
</doctrine-mapping>
Como vemos acima o elemento raiz é doctrine-mapping
, dentro dele colocamos o elemento entity
que será o mapeamento da entidade, ele recebe como atributo o name
(nome da classe com namespace
) e o nome da tabela. A chave primaria é mapeada utilizando o elemento id
sendo o seu atributo name
o nome da coluna e type
seu tipo como apresentado acima Dentro do elemento id
adicionamos o seu gerador através do elemento generator
que recebe como atributo a estratégia de geração de chave primaria.
Cada uma das colunas é mapeada utilizando um elemento field
com o atributo name
o nome da coluna e type
seu tipo dos quais foram mostrados anteriormente, também podemos adicionar como atributos as configurações como length
, nullable
, etc.
Chaves Primarias
Como vimos para mapear a chave primaria da tabela basta anotá-la com @ORM\Id
se tiver utilizando anotações ou com o elemento id
no XML. Para chaves auto incrementas devemos informar qual estratégia de geração queremos utilizar, com anotações utilizamos @ORM\GeneratedValue
que sem nenhum argumento irá utilizar a estratégia AUTO
, já em XML devemos informar através do elemento <generator
strategy="AUTO"
/>
. Podemos utilizar diversos tipos de estratégias de geração embora na maior parte das vezes o que queremos é a padrão mesmo.
Veja abaixo uma lista com as estratégias suportadas:
AUTO
(padrão): Diz ao Doctrine utilizar a estratégia da plataforma de banco de dados usada. As estratégias preferidas são IDENTITY para MySQL, SQLite, MsSQL e SEQUENCE para Oracle e PostgreSQL. Esta estratégia prove uma boa portabilidade.SEQUENCE
: Diz ao Doctrine usar uma sequencia de banco de dados para a geração de ID. Esta estratégia é suportada por Oracle e PostgreSql.IDENTITY
: Diz ao Doctrine para usar uma coluna especial de identidade que gera um valor ao inserir uma linha. Esta estratégia é suportado por MySQL/SQLite(AUTO\_INCREMENT), MSSQL (IDENTITY) e PostgreSQL (SERIAL).UUID
: Diz ao Doctrine para usar um gerador built-in Universally Unique Identifier. Esta estratégia prove uma boa portabilidade.
Então para utilizarmos uma estratégia de UUID podemos utilizar:
/**
* @ORM\Id
* @ORM\Column(type="string")
* @ORM\GeneratedValue(strategy="UUID")
*/
private $id;
ou para uma sequencia
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="SEQUENCE")
* @ORM\SequenceGenerator(sequenceName="posts_seq", initialValue=1, allocationSize=100)
*
*/
private $id;
No caso de utilizarmos uma estratégia de sequencia(SEQUENCE) podemos configurar o gerador fornecendo um nome sequenceName
, um valor inicial de inicio initialValue
, e qual o incremento de cada novo valor gerado allocationSize
.
O Doctrine também permite o uso de chaves primaria composta, bastando para isso marcar dois atributos com a anotação @ORM\Id
.
Conectando ao Banco
Para manipular nossas entidades iremos necessitar criar um objeto EntityManager
, e para criar este objeto devemos conectar ao banco e carragar nosso mapeamento de entidades.
No nosso exemplo vamos criar um arquivo bootstrap-doctrine.php
para fazer isso.
<?php
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Setup;
require_once "vendor/autoload.php";
$isDevMode = true;
$proxyDir = null;
$cache = null;
$configAnotacoes = Setup::createAnnotationMetadataConfiguration(
array(__DIR__."/src"),
$isDevMode,
$proxyDir,
$cache,
false
);
//$configXML = Setup::createXMLMetadataConfiguration(array(__DIR__."/config"), $isDevMode);
$params1 = array(
'driver' => 'pdo_mysql',
'user' => 'usuario',
'password' => 'senha',
'dbname' => 'banco',
'host' => 'localhost'
);
//$params2 = [ 'url'=> "mysql://usuario:senha@localhost/banco;charset=utf8mb4" ];
$entityManager = EntityManager::create($params1, $configAnotacoes);
Como vimos para criar um objeto EntityManager
(linha 31) precisamos de dois argumentos, primeiro as configurações de conexão ao banco, que recebe um array, podendo ser informado tanto do os valores separados com suas devidas chaves associadas como mostrado das linhas 22 a 28, ou como uma string de conexão, como mostrado comentado na linha 29.
Como segundo parâmetro ele recebe um objeto de configuração, criação deste objeto irá mudar se estamos utilizando um mapeamento por Anotações ou por XML. Por anotações criamos utilizando Setup::createAnnotationMetadataConfiguration
passando como argumentos um array de diretórios onde esta nossas classes anotadas( array(__DIR__."/src")
), se estamos no em modo de desenvolvedor($isDevMode
), nosso diretório de proxies ($proxyDir), um objeto de cache ($cache
não configurado nesse exemplo).
Note que neste exemplo estamos utilizando configurações de desenvolvimento simples, não recomendadas para uma aplicação em produção, fazemos isso para deixar mais simples, pois este é um artigo apenas de introdução, para melhores configurações e desempenho consulte a documentação.
Na linha 20 esta comentado a configuração para o mapeamento de entidades via XML. Informamos como argumento um array com os diretórios onde se encontram os arquivo XML de mapeamento.
Criando as tabelas no banco
Um recurso muito interessante do Doctrine é que ele possui uma interface de linha de comando para criar o schema do banco de dado. Para utilizá-la precisamos criar um arquivo cli-config.php
<?php
use Doctrine\ORM\Tools\Console\ConsoleRunner;
require_once 'bootstrap-doctrine.php';
return ConsoleRunner::createHelperSet($entityManager);
Nele apenas incluímos nosso arquivo bootstrap-doctrine.php
que contem a configuração e criação do EntityManager
e passamos esse objeto para o ConsoleRunner::createHelperSet
, que o retorno deve ser retornado.
Agora podemos utilizar os comando no prompt. Para criar os schema utilizamos:
vendor/bin/doctrine orm:schema-tool:create
Ele irá criar o schema do banco de dados no nosso banco, também podemos passar o argumento --dump-sql
que ao invés de criar irá mostrar os comando SQL que irá ser executado.
Se alterarmos algo no nosso mapeamento que deve refletir para o banco podemos realizar as alterações da seguinte forma:
vendor/bin/doctrine orm:schema-tool:update --force
Se precisarmos deletar todo o schema podemos utilizar o drop
:
vendor/bin/doctrine orm:schema-tool:drop --force
Manipulando Entidades no banco
Para inserir um valor no banco utilizamos o objeto $entityManager
criado no arquivo bootstrap-doctrine.php
, então começamos incluindo ele no nosso script, criando um objeto Post e persistindo.
<?php
require 'vendor/autoload.php';
require_once 'bootstrap-doctrine.php';
use BotecoDigital\Post;
$post = new Post();
$post->setTitulo('O Titulo do post ');
$post->setTexto('Vivamus et semper lacus. Fusce ac purus quis justo faucibus ....');
$post->setDataPublicacao((new DateTime()));
$entityManager->persist($post);
$entityManager->flush();
Persistimos um objeto através do método persist
do $entityManager
passando o objeto que desejar salvar, lembrando que devemos ao final chamar o flush()
para efetivamente realizar a persistência no banco.
Lembrando que persist
serve tanto para inserir como para atualizar.
Para recuperar um objeto salvo podemos utilizar o método find
, passando a classe do objeto de entidade que queremos recuperar seguido pelo seu idenficador.
$post = $entityManager->find(Post::class, 1);
echo $post->getId() . PHP_EOL;
echo $post->getTitulo() . PHP_EOL;
echo $post->getTexto() . PHP_EOL;
echo $post->getDataPublicacao()->format('d/m/Y H:i:s') . PHP_EOL;
Podemos buscar todos os objetos através do método findAll
:
$postRepository = $entityManager->getRepository(Post::class);
$posts = $postRepository->findAll();
foreach ($posts as $post) {
echo $post->getTitulo() . PHP_EOL;
}
Primeiro pegamos um objeto de repositório da classe Post e depois chamamos o método findAll
.
Também podemos realizar alguns filtros utilizando o findBy
:
$postRepository = $entityManager->getRepository(Post::class);
$posts = $postRepository->findBy(["titulo" => 'Titulo do Post']);
foreach ($posts as $post) {
echo $post->getTitulo() . PHP_EOL;
}
Ele recebe como argumento um array que será o critério da consulta, $orderBy
, $limit
e $offset
. Para consultas mais complexas pode-se criar um objeto especial de query, veremos mais adiante.
Para remover uma entidade basta chamar o método remove
:
$post = $entityManager->find(Post::class, 1);
$entityManager->remove($post);
$entityManager->flush();
O método remove recebe um objeto que já esteja sendo gerenciado pelo Doctrine, ou seja, recuperado por um dos seus métodos, caso você crie um objeto na mão e tente passar para o método ele irá dar o erro de “Detached entity”. E lembrando para efetivar a remoção no bando devemos chamar o método flush()
.
Relação entre entidades
No Doctrine você não trabalha com chaves estrangeiras, mas sim com referencias a objetos, e o o próprio Doctrine se encarrega de converter estas referencias para chaves estrangeiras.
Muitos para Um – Many to One(n-1)
É uma associação muito comum, por exemplo, muitos Post tem uma categoria. Podemos mapear da seguinte forma:
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="posts")
*/
class Post
{
// outros atributos
/**
* @ORM\ManyToOne(targetEntity="Categoria")
* @ORM\JoinColumn(name="categoria_id", referencedColumnName="id")
*/
private $categoria;
}
/**
* @ORM\Entity
* @ORM\Table(name="categorias")
*/
class Categoria
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(name="id", type="integer")
*/
private $id;
/** @ORM\Column(type="string") */
private $nome;
}
Para realizar o mapeamento anotamos o atributo que terá a referencia para outra entidade com a anotação @ORM\ManyToOne
passando como argumento o nome da entidade que queremos associar targetEntity="Categoria"
. Também anotamos o atributo com @ORM\JoinColumn
informando o nome da coluna que será nossa chave estrangeira(name="categoria_id"
) e qual coluna ela esta associada referencedColumnName="id"
.
A associação será mapeada em SQL da seguinte maneira:
CREATE TABLE categorias (
id INT AUTO_INCREMENT NOT NULL,
nome VARCHAR(255) NOT NULL,
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE posts (
id INT AUTO_INCREMENT NOT NULL,
categoria_id INT DEFAULT NULL,
titulo VARCHAR(255) NOT NULL,
texto LONGTEXT NOT NULL,
dataPublicacao DATETIME NOT NULL,
INDEX IDX_885DBAFA3397707A (categoria_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
ALTER TABLE posts ADD CONSTRAINT FK_885DBAFA3397707A FOREIGN KEY (categoria_id) REFERENCES categorias (id);
Também podemos realizar o mapeamento da associação através de XML:
<doctrine-mapping>
<entity name="BotecoDigital\Post" table="posts">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<!-- outros campos -->
<many-to-one field="categoria" target-entity="Categoria">
<join-column name="categoria_id" referenced-column-name="id" />
</many-to-one>
</entity>
</doctrine-mapping>
Uma para Um – One to One (1-1) unidirecional
Quando uma Entidade esta associada a uma ou única outra entidade, por exemplo, um gerente está associado a um único departamento e um departamento está associado a um único gerente. Vejamos um exemplo:
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="gerentes")
*/
class Gerente
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/** @ORM\Column(type="string") */
private $nome;
/**
* @ORM\OneToOne(targetEntity="Departamento")
* @ORM\JoinColumn(name="departamento_id", referencedColumnName="id")
*/
private $departamento;
}
/**
* @ORM\Entity
* @ORM\Table(name="departamentos")
*/
class Departamento
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/** @ORM\Column(type="string") */
private $nome;
}
Para associar o Gerente ao Departamento utilizamos a anotação @ORM\OneToOne
com o argumento indicando a entidade de Departamento targetEntity="Departamento"
. Definimos também as informações de associação através da anotação @ORM\JoinColumn(name="departamento_id", referencedColumnName="id")
, informando o nome da coluna de chave estrangeira e qual coluna ela referencia. O mapeamento irá gerar o seguinte SQL:
CREATE TABLE departamentos (
id INT AUTO_INCREMENT NOT NULL,
nome VARCHAR(255) NOT NULL,
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE gerentes (
id INT AUTO_INCREMENT NOT NULL,
departamento_id INT DEFAULT NULL,
nome VARCHAR(255) NOT NULL,
UNIQUE INDEX UNIQ_283DBEA65A91C08D (departamento_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
ALTER TABLE gerentes ADD CONSTRAINT FK_283DBEA65A91C08D FOREIGN KEY (departamento_id) REFERENCES departamentos (id);
Também podemos mapear via XML esta associação:
<doctrine-mapping>
<entity name="BotecoDigital\Gerente" table="gerentes">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<field name="nome" type="string" />
<one-to-one field="departamento" target-entity="Departamento">
<join-column name="departamento_id" referenced-column-name="id" />
</one-to-one>
</entity>
</doctrine-mapping>
Uma para Um – One to One (1-1) bidirecional
A associação anterior era uma associação unidirecional, ou seja, somente um dos lados, no caso Gerente, poderia acessar o outro. Podemos criar uma associação bidirecional;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="gerentes")
*/
class Gerente
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/** @ORM\Column(type="string") */
private $nome;
/**
* @ORM\OneToOne(targetEntity="Departamento", inversedBy="gerente")
* @ORM\JoinColumn(name="departamento_id", referencedColumnName="id")
*/
private $departamento;
}
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="departamentos")
*/
class Departamento
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/** @ORM\Column(type="string") */
private $nome;
/**
* @ORM\OneToOne(targetEntity="Gerente", mappedBy="departamento")
*/
private $gerente;
}
O mapeamento do Gerente é muito parecido com o anterior, apenas adicionamos o argumento inversedBy="gerente"
, isso informa ao Doctrine que o gerente está mapeado na entidade Departamento pelo atributo $departamento
, ou seja, qual é o mapeamento do outro lado.
No mapeamento do Departamento adicionamos um atributo $gerente
e o anotamos com @ORM\OneToOne(targetEntity="Gerente", mappedBy="departamento")
, associado a gerente e informando por mappedBy
que o departamento é mapeado no Gerente pelo atributo $departamento
.
O mapeamento irá gerar o seguinte XML:
CREATE TABLE departamentos (
id INT AUTO_INCREMENT NOT NULL,
nome VARCHAR(255) NOT NULL,
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE gerentes (
id INT AUTO_INCREMENT NOT NULL,
departamento_id INT DEFAULT NULL,
nome VARCHAR(255) NOT NULL,
UNIQUE INDEX UNIQ_283DBEA65A91C08D (departamento_id), PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
ALTER TABLE gerentes ADD CONSTRAINT FK_283DBEA65A91C08D FOREIGN KEY (departamento_id) REFERENCES departamentos (id);
Em XML o mapeamento ficaria assim:
<!-- BotecoDigital.Gerente.dcm.xml -->
<doctrine-mapping>
<entity name="BotecoDigital\Gerente" table="gerentes">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<field name="nome" type="string" />
<one-to-one field="departamento" target-entity="Departamento" inversed-by="gerente">
<join-column name="departamento_id" referenced-column-name="id" />
</one-to-one>
</entity>
</doctrine-mapping>
<!-- BotecoDigital.Departamento.dcm.xml -->
<doctrine-mapping>
<entity name="BotecoDigital\Departamento" table="departamentos">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<field name="nome" type="string" />
<one-to-one field="gerente" target-entity="Gerente" mapped-by="departamento" />
</entity>
</doctrine-mapping>
Um para Muitos – One to Many (1-n) Bidirecional
A associação um para muitos também é muito comum, podemos dizer que ela é o outro lado da muitos-para-um, um exemplo clássico desta associação é um Post tem muitos Comentários, vamos a um exemplos:
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="posts")
*/
class Post
{
// outros atributos
/**
* @ORM\OneToMany(targetEntity="Comentario", mappedBy="post")
*/
private $comentarios;
}
/**
* @ORM\Entity
* @ORM\Table(name="comentarios")
*/
class Comentario
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/** @ORM\Column(type="string") */
private $nome;
/** @ORM\Column(type="text") */
private $comentario;
/**
* @ORM\ManyToOne(targetEntity="Post", inversedBy="comentarios")
* @ORM\JoinColumn(name="post_id", referencedColumnName="id")
*/
private $post;
}
Na entidade Post adicionamos o atributo comentários, nele mapeamos através da anotação @ORM\OneToMany(targetEntity="Comentario", mappedBy="post")
a relação com a entidade Comentario onde um post está mapeado pelo atributo $post.
Na entidade Comentario mapeamos o post utilizando a anotação @ORM\ManyToOne(targetEntity="Post", inversedBy="comentarios")
, informando que o comentario está mapeado na entidade Post através do atributo $comentarios.
Vejamos como fica o mapeamento em SQL
CREATE TABLE comentarios (
id INT AUTO_INCREMENT NOT NULL,
post_id INT DEFAULT NULL,
nome VARCHAR(255) NOT NULL,
comentario LONGTEXT NOT NULL,
INDEX IDX_F54B3FC04B89032C (post_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE posts (
id INT AUTO_INCREMENT NOT NULL,
categoria_id INT DEFAULT NULL,
titulo VARCHAR(255) NOT NULL,
texto LONGTEXT NOT NULL,
dataPublicacao DATETIME NOT NULL,
INDEX IDX_885DBAFA3397707A (categoria_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
ALTER TABLE comentarios ADD CONSTRAINT FK_F54B3FC04B89032C FOREIGN KEY (post_id) REFERENCES posts (id);
O mapeamento em XML ficaria:
<!-- BotecoDigital.Post.dcm.xml -->
<doctrine-mapping>
<entity name="BotecoDigital\Post" table="posts">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<!-- outros campos -->
<many-to-one field="categoria" target-entity="Categoria">
<join-column name="categoria_id" referenced-column-name="id" />
</many-to-one>
<one-to-many field="comentarios" target-entity="Comentario" mapped-by="post" />
</entity>
</doctrine-mapping>
<!-- BotecoDigital.Comentario.dcm.xml -->
<doctrine-mapping>
<entity name="BotecoDigital\Comentario" table="comentarios">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<field name="nome" type="string" />
<field name="comentario" type="text" />
<many-to-one field="post" target-entity="Post" inversed-by="comentarios">
<join-column name="post_id" referenced-column-name="id" />
</many-to-one>
</entity>
</doctrine-mapping>
Muitos para Muitos – Many to Many (n-n)
Associações muitos-para-muitos não são tão comuns e um pouco complexas já que utilizam uma terceira tabela para associar as duas entidades envolvidas. Um exemplo desse tipo de associação pode ser a relação de Post e PalavrasChaves, um post pode ter muitas palavras chaves e uma PalavraChave pode aparecer em muitos posts.
/**
* @ORM\Entity
* @ORM\Table(name="posts")
*/
class Post
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue()
*
*/
private $id;
// outros atributos
/**
* @ORM\ManyToMany(targetEntity="PalavraChave")
* @ORM\JoinTable(
* name="post_palavraschaves",
* joinColumns={@ORM\JoinColumn(name="post_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="palavrachave_id", referencedColumnName="id")}
* )
*/
private $palavrasChaves;
}
/**
* @ORM\Entity
* @ORM\Table(name="palavraschaves")
*/
class PalavraChave
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/** @ORM\Column(type="string") */
private $palavra;
/**
* @ORM\ManyToMany(targetEntity="Post", mappedBy="palavrasChaves")
*/
private $posts;
}
Para realizar o mapeamento criamos o atributo $palavrasChaves
na entidade Post, e o marcamos ele com a anotação @ORM\ManyToMany(targetEntity="PalavraChave")
informando a associação com a entidade PalavraChave. Também devemos anotar este atributo com as configurações da tabela de associação, isto é feito utilizando a anotação @ORM\JoinTable
, que recebe três argumentos, o primeiro é o name
que será o nome da tabela de associação que será criada, o joinColumns
que recebe outra anotação sendo esta a definição da coluna que será a chave estrangeira para a própria entidade de Post e o terceiro é o inverseJoinColumns
que define a coluna de chave estrangeira(relação) para a tabela que estamos associando no caso PalavraChave.
Se necessitarmos que esta relação seja bidirecional basta anotar na entidade PalavraChave o atributo $posts
com a anotação @ORM\ManyToMany(targetEntity="Post", mappedBy="palavrasChaves")
, informando o nome da entidade que estamos associando( argumento targetEntity
) e em qual atributo é mapeada a relação(mappedBy
).
O mapeamento irá gerar o seguinte SQL:
CREATE TABLE palavraschaves (
id INT AUTO_INCREMENT NOT NULL,
palavra VARCHAR(255) NOT NULL,
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE posts (
id INT AUTO_INCREMENT NOT NULL,
categoria_id INT DEFAULT NULL,
titulo VARCHAR(255) NOT NULL,
texto LONGTEXT NOT NULL,
dataPublicacao DATETIME NOT NULL,
INDEX IDX_885DBAFA3397707A (categoria_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE post_palavraschaves (
post_id INT NOT NULL,
palavrachave_id INT NOT NULL,
INDEX IDX_11939FCF4B89032C (post_id),
INDEX IDX_11939FCF8E2FDB7D (palavrachave_id),
PRIMARY KEY(post_id, palavrachave_id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
ALTER TABLE posts ADD CONSTRAINT FK_885DBAFA3397707A FOREIGN KEY (categoria_id) REFERENCES categorias (id);
ALTER TABLE post_palavraschaves ADD CONSTRAINT FK_11939FCF4B89032C FOREIGN KEY (post_id) REFERENCES posts (id);
ALTER TABLE post_palavraschaves ADD CONSTRAINT FK_11939FCF8E2FDB7D FOREIGN KEY (palavrachave_id) REFERENCES palavraschaves (id);
O Mapeamento em XML ficaria:
<!-- BotecoDigital.Post.dcm.xml -->
<doctrine-mapping>
<entity name="BotecoDigital\Post" table="posts">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<field name="titulo" type="string" />
<field name="texto" type="text" />
<field name="dataPublicacao" type="datetime" />
<many-to-one field="categoria" target-entity="Categoria">
<join-column name="categoria_id" referenced-column-name="id" />
</many-to-one>
<one-to-many field="comentarios" target-entity="Comentario" mapped-by="post" />
<many-to-many field="palavrasChaves" inversed-by="posts" target-entity="PalavraChave">
<join-table name="post_palavraschaves">
<join-columns>
<join-column name="post_id" referenced-column-name="id" />
</join-columns>
<inverse-join-columns>
<join-column name="palavrachave_id" referenced-column-name="id" />
</inverse-join-columns>
</join-table>
</many-to-many>
</entity>
</doctrine-mapping>
<!-- BotecoDigital.PalavraChave.dcm.xml -->
<doctrine-mapping>
<entity name="BotecoDigital\PalavraChave" table="palavraschaves">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<field name="palavra" type="string" />
<many-to-many field="posts" mapped-by="palavrasChaves" target-entity="Post"/>
</entity>
</doctrine-mapping>
Salvando relacionamentos
Para salvar entidades relacionadas devemos em principio salvar cada uma das entidades separadamente.
require_once 'bootstrap-doctrine.php';
use BotecoDigital\Comentario;
use BotecoDigital\Post;
$post = new Post();
$post->setTitulo("titulo teste");
$post->setTexto("um texto");
$post->setDataPublicacao(new DateTime());
$entityManager->persist($post);
$c1 = new Comentario();
$c1->setNome("Rodrigo");
$c1->setComentario("um comentario 1");
$c1->setPost($post);
$c2 = new Comentario();
$c2->setNome("Ana");
$c2->setComentario("um comentario 2");
$c2->setPost($post);
$entityManager->persist($c1);
$entityManager->persist($c2);
$entityManager->flush();
$d = $entityManager->find(Post::class, 1);
$entityManager->refresh($d);
foreach ($d->getComentarios() as $key => $value) {
echo $value->getNome().PHP_EOL;
}
Para não termos todo este trabalho, podemos adicionar na anotação de mapeamento o argumento cascade={"persist"}
, ele permitirá que ao ter uma entidade não persistida em um dos atributos de relação da entidade que estamos salvando esta será persistida automaticamente.
/**
* @ORM\OneToMany(targetEntity="Comentario", mappedBy="post" ,cascade={"persist"})
*/
private $comentarios;
.....
$post = new Post();
$post->setTitulo("titulo teste");
$post->setTexto("um texto");
$post->setDataPublicacao(new DateTime());
$c1 = new Comentario();
$c1->setNome("Rodrigo");
$c1->setComentario("um comentario 1");
$c1->setPost($post);
$c2 = new Comentario();
$c2->setNome("Ana");
$c2->setComentario("um comentario 2");
$c2->setPost($post);
$post->setComentarios([$c1,$c2]);
$entityManager->persist($post);
$entityManager->flush();
Como podemos persistir em cascata podemos deletar em cascata, bastando adicionar remove
ao argumento de da anotação. Mas cuidado você pode acabar deletando algo indevido desta forma.
/**
* @ORM\OneToMany(targetEntity="Comentario", mappedBy="post" ,cascade={"persist","remove"})
*/
private $comentarios;
Construindo Querys
Doctrine utiliza uma linguagem de consulta chamada DQL(Doctrine Query Language), o conceito é similar JPQL do Java, permitindo realizar consultas sobre as entidade de uma maneira muito similar ao SQL. Mas lembrando não é exatamente SQL. Vamos a um exemplo:
$query = $entityManager->createQuery("SELECT c FROM BotecoDigital\Categoria c WHERE c.nome LIKE 'Art%'");
$categorias = $query->getResult();
foreach ($categorias as $cat) {
echo $cat->getId() . ' - ' . $cat->getNome() . PHP_EOL;
}
Como vemos criamos uma consulta/query utilizando o método createQuey
passando a DQL. Nela temos o SELECT c
onde c
é um aliases para a nossa entidade a ser buscada, sendo assim retornará um objeto com os valores dos atributos preenchidos. Também temos o FROM BotecoDigital\Categoria c
especificando de qual entidade estamos buscando, lembrando que deve ser o nome totalmente qualificado(fully-qualified) da classe de entidade seguida de um identificador, e finalmente uma cláusula de filtro WHERE c.nome LIKE 'Art%'
onde o atributo nome da entidade c deve começar com Art.
Com um objeto $query
chamamos o método getResult()
que irá buscar no banco e retornar um array de objetos correspondente a DQL executada.
Se sabemos que o DQL retornará somente 1 resultado podemos utilizar o método getSingleResult()
, ou se iremos retornar somente um resultado que não seja um objeto como na seguinte DQL "SELECT count(c.id) FROM BotecoDigital\Categoria c"
podemos utilizar o getSingleScalarResult()
.
Joins
Para realizar o JOIN de duas entidades utilizamos simplesmente a própria instrução JOIN que deve ser seguido do nome fully-qualified da entidade a ser associada seguido de um identificador.
$query = $entityManager->createQuery("SELECT p FROM BotecoDigital\Post p JOIN Botecodigital\Categoria c WHERE p.categoria = 1");
$posts = $query->getResult();
foreach ($posts as $p) {
echo $p->getTitulo() . PHP_EOL;
echo $p->getCategoria()->getNome() . PHP_EOL;
}
Consulta com parâmetros
Para utilizarmos parâmetros em nossas consultas simplesmente colocamos um marcador :identificador
na posição onde se deseja inserir o valor e depois configurar o valor através do método setParameter
, passando como argumento o identificador do marcador e o valor.
$query = $entityManager->createQuery("SELECT p FROM BotecoDigital\Post p JOIN Botecodigital\Categoria c WHERE p.categoria = :id");
$query->setParameter('id', 1);
$posts = $query->getResult();
foreach ($posts as $p) {
echo $p->getTitulo() . PHP_EOL;
echo $p->getCategoria()->getNome() . PHP_EOL;
}
Consultas Construidas/BuilderQuery
Em alguns casos pode ser trabalhoso criar o DQL, principalmente se a consulta dependa de diversos condicionais. Para isso pode-se utilizar o QueryBuilder, que fornece diversos métodos para a construção da DQL.
$qb = $entityManager->createQueryBuilder();
$qb->select('p')
->from(Post::class, 'p')
->join(Categoria::class, 'c')
->where('p.categoria = :id')
->orderBy('p.titulo', 'ASC');
$query = $qb->getQuery();
$query->setParameter('id',1);
$posts = $query->getResult();
foreach ($posts as $p) {
echo $p->getTitulo() . PHP_EOL;
echo $p->getCategoria()->getNome() . PHP_EOL;
}
Entre os métodos métodos disponíveis para a construção da consulta temos também o setFirstResult(int)
para determinar o offset da consulta e o setMaxResults(int)
para definir o número de resultados retornado.
Bom o post já ficou maior que o previsto, mas espero que tenha sido um introdução útil. Para maiores detalhes consulte a documenta do Doctrine.