Documentação do Symfony - versão 3.1
Renderizada do repositório symfony-docs-pt-BR no Github
Temos que dizer, uma das tarefas mais comuns e desafiadoras em qualquer aplicação envolve persistir e ler informações de um banco de dados. Felizmente o Symfony vem integrado com o Doctrine, uma biblioteca cujo único objetivo é fornecer poderosas ferramentas que tornem isso fácil. Nesse capítulo você aprenderá a filosofia básica por trás do Doctrine e verá quão fácil pode ser trabalhar com um banco de dados.
Note
O Doctrine é totalmente desacoplado do Symfony, e seu uso é opcional. Esse capítulo é totalmente sobre o Doctrine ORM, que visa permitir fazer mapeamento de objetos para um banco de dados relacional (como o MySQL, PostgreSQL ou o Microsoft SQL). É fácil usar consultas SQL puras se você preferir, isso é explicado na entrada do cookbook “/cookbook/doctrine/dbal”.
Você também pode persistir dados no MongoDB usando a biblioteca Doctrine ODM. Para mais informações, leia a documentação “/bundles/DoctrineMongoDBBundle/index”.
O jeito mais fácil de entender como o Doctrine trabalha é vendo-o em ação.
Nessa seção, você configurará seu banco de dados, criará um objeto Product
,
fará sua persistência no banco e depois irá retorná-lo.
Antes de começar realmente, você precisa configurar a informação de conexão do
seu banco. Por convenção, essa informação geralmente é configurada no arquivo
app/config/parameters.yml
:
1 2 3 4 5 6 7 | # app/config/parameters.yml
parameters:
database_driver: pdo_mysql
database_host: localhost
database_name: test_project
database_user: root
database_password: password
|
Note
Definir a configuração pelo parameters.yml
é apenas uma convenção. Os
parâmetros definidos naquele arquivo são referenciados pelo arquivo de
configuração principal na configuração do Doctrine:
1 2 3 4 5 6 7 | doctrine:
dbal:
driver: %database_driver%
host: %database_host%
dbname: %database_name%
user: %database_user%
password: %database_password%
|
Colocando a informação do banco de dados em um arquivo separado, você pode manter de forma fácil diferentes versões em cada um dos servidores. Você pode também guardar facilmente a configuração de banco (ou qualquer outra informação delicada) fora do seu projeto, por exemplo dentro do seu diretório de configuração do Apache. Para mais informações, de uma olhada em /cookbook/configuration/external_parameters.
Agora que o Doctrine sabe sobre seu banco, pode deixar que ele faça a criação dele para você:
1 | php app/console doctrine:database:create
|
Suponha que você esteja criando uma aplicação onde os produtos precisam ser
mostrados. Antes mesmo de pensar sobre o Doctrine ou banco de dados, você já
sabe que irá precisar de um objeto Product
para representar esses produtos.
Crie essa classe dentro do diretório Entity
no seu bundle
AcmeStoreBundle
:
// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
class Product
{
protected $name;
protected $price;
protected $description;
}
A classe - frequentemente chamada de “entidade”, que significa uma classe básica para guardar dados - é simples e ajuda a cumprir o requisito de negócio referente aos produtos na sua aplicação. Essa classe ainda não pode ser persistida no banco de dados - ela é apenas uma classe PHP simples.
Tip
Depois que você aprender os conceitos por trás do Doctrine, você pode deixá-lo criar essa classe entidade para você:
1 | php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Product" --fields="name:string(255) price:float description:text"
|
O Doctrine permite que você trabalhe de uma forma muito mais interessante com banco de dados do que apenas buscar registros de uma tabela baseada em colunas para um array. Em vez disso, o Doctrine permite que você persista objetos inteiros no banco e recupere objetos inteiros do banco de dados. Isso funciona mapeando uma classe PHP com uma tabela do banco, e as propriedades dessa classe com as colunas da tabela:
Para o Doctrine ser capaz disso, você tem apenas que criar “metadados”, em
outras palavras a configuração que diz ao Doctrine exatamente como a classe
Product
e suas propriedades devem ser mapeadas com o banco de dados.
Esses metadados podem ser especificados em vários diferentes formatos incluindo
YAML, XML ou diretamente dentro da classe Product
por meio de annotations:
Note
Um bundle só pode aceitar um formato para definição de metadados. Por exemplo, não é possível misturar definições em YAML com definições por annotations nas classes entidade.
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 | // src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="product")
*/
class Product
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(type="string", length=100)
*/
protected $name;
/**
* @ORM\Column(type="decimal", scale=2)
*/
protected $price;
/**
* @ORM\Column(type="text")
*/
protected $description;
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | # src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
type: entity
table: product
id:
id:
type: integer
generator: { strategy: AUTO }
fields:
name:
type: string
length: 100
price:
type: decimal
scale: 2
description:
type: text
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Acme\StoreBundle\Entity\Product" table="product">
<id name="id" type="integer" column="id">
<generator strategy="AUTO" />
</id>
<field name="name" column="name" type="string" length="100" />
<field name="price" column="price" type="decimal" scale="2" />
<field name="description" column="description" type="text" />
</entity>
</doctrine-mapping>
|
Tip
O nome da tabela é opcional e, se omitido, será determinado automaticamente baseado no nome da classe entidade.
O Doctrine permite que você escolha entre uma grande variedade de diferentes tipos de campo, cada um com suas opções específicas. Para informações sobre os tipos de campos disponíveis, dê uma olhada na seção Referência dos Tipos de Campos do Doctrine.
See also
Você também pode conferir a Documentação Básica sobre Mapeamento do
Doctrine para todos os detalhes sobre o tema. Se você usar annotations,
irá precisar prefixar todas elas com ORM\
(i.e. ORM\Column(..)
),
o que não é citado na documentação do Doctrine. Você também irá precisar
incluir o comando use Doctrine\ORM\Mapping as ORM;
, que importa o
prefixo ORM
das annotations.
Caution
Tenha cuidado para que o nome da sua classe e suas propriedades não estão
mapeadas com o nome de um comando SQL protegido (como group``ou
``user
). Por exemplo, se o nome da sua classe entidade é Group
então,
por padrão, o nome da sua tabela será group
, que causará um erro de
SQL em alguns dos bancos de dados. Dê uma olhada na Documentação sobre
os nomes de comandos SQL reservados para ver como escapar adequadamente
esses nomes. Alternativamente, se você pode escolher livremente seu
esquema de banco de dados, simplesmente mapeie para um nome de tabela
ou nome de coluna diferente. Veja a documentação do Doctrine sobre
`Classes persistentes`_ e Mapeamento de propriedades
Note
Quando usar outra biblioteca ou programa (i.e. Doxygen) que usa annotations
você dever colocar a annotation @IgnoreAnnotation
na classe para indicar
que annotations o Symfony deve ignorar.
Por exemplo, para prevenir que a annotation @fn
gere uma exceção, inclua
o seguinte:
- /**
- @IgnoreAnnotation(“fn”)
*/
class Product
Apesar do Doctrine agora saber como persistir um objeto Product
num banco
de dados, a classe ainda não é realmente útil. Como Product
é apenas uma
classe PHP usual, você precisa criar os métodos getters e setters (i.e.
getName()
, setName()
para acessar sua suas propriedades (até as
propriedades protected
). Felizmente o Doctrine pode fazer isso por você
executando:
1 | php app/console doctrine:generate:entities Acme/StoreBundle/Entity/Product
|
Esse comando garante que todos os getters e setters estão criados na classe
Product
. Ele é um comando seguro - você pode executá-lo muitas e muitas
vezes: ele apenas gera getters e setters que ainda não existem (i.e. ele não
altera os models já existentes).
Caution
O comando doctrine:generate:entities
gera um backup do Product.php
original chamado de ``Product.php~`. Em alguns casos, a presença desse
arquivo pode causar um erro “Cannot redeclare class`. É seguro removê-lo.
Você pode gerar todas as entidades que são conhecidas por um bundle (i.e. cada classe PHP com a informação de mapeamento do Doctrine) ou de um namespace inteiro.
1 2 | php app/console doctrine:generate:entities AcmeStoreBundle
php app/console doctrine:generate:entities Acme
|
Note
O Doctrine não se importa se as suas propriedades são protected
ou
private
, ou se você não tem um método getter ou setter. Os getters e
setters são gerados aqui apenas porque você irá precisar deles para
interagir com o seu objeto PHP.
Agora você tem uma classe utilizável Product
com informação de mapeamento
assim o Doctrine sabe exatamente como fazer a persistência dela. É claro, você
ainda não tem a tabela correspondente product
no seu banco de dados.
Felizmente, o Doctrine pode criar automaticamente todas as tabelas necessárias
no banco para cada uma das entidades conhecidas da sua aplicação. Para isso,
execute:
1 | php app/console doctrine:schema:update --force
|
Tip
Na verdade, esse comando é extremamente poderoso. Ele compara o que o banco
de dados deveria se parecer (baseado na informação de mapeamento das suas
entidades) com o que ele realmente se parece, e gera os comandos SQL
necessários para atualizar o banco para o que ele deveria ser. Em outras
palavras, se você adicionar uma nova propriedade com metadados de
mapeamento na classe Product``e executar esse comando novamente, ele irá
criar a instrução ''alter table'' para adicionar as novas colunas na tabela
``product
existente.
Uma maneira ainda melhor de se aproveitar dessa funcionalidade é por meio das migrations, que lhe permitem criar essas instruções SQL e guardá-las em classes migration que podem ser rodadas de forma sistemática no seu servidor de produção para que se possa acompanhar e migrar o schema do seu banco de dados de uma forma mais segura e confiável.
Seu banco de dados agora tem uma tabela product
totalmente funcional com
as colunas correspondendo com os metadados que foram especificados.
Agora que você tem uma entidade Product
mapeada e a tabela correspondente
product
, já está pronto para persistir os dados no banco. De dentro de um
controller, isso é bem simples. Inclua o seguinte método no
DefaultController
do bundle:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // src/Acme/StoreBundle/Controller/DefaultController.php
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
// ...
public function createAction()
{
$product = new Product();
$product->setName('A Foo Bar');
$product->setPrice('19.99');
$product->setDescription('Lorem ipsum dolor');
$em = $this->getDoctrine()->getManager();
$em->persist($product);
$em->flush();
return new Response('Created product id '.$product->getId());
}
|
Note
Se você estiver seguindo o exemplo na prática, precisará criar a rota que aponta para essa action se quiser vê-la funcionando.
Vamos caminhar pelo exemplo:
$product
como
qualquer outro objeto PHP normal;persist()
diz ao Doctrine para ‘’gerenciar’’ o
objeto $product
. Isso não gera (ainda) um comando real no banco de dados.flush()
é chamado, o Doctrine verifica em
todos os objetos que ele gerencia para ver se eles necessitam ser persistidos
no banco. Nesse exemplo, o objeto $product
ainda não foi persistido, por
isso o entity manager executa um comando INSERT
e um registro é criado
na tabela product
.Note
Na verdade, como o Doctrine conhece todas as entidades gerenciadas,
quando você chama o método flush()
, ele calcula um changeset geral e
executa o comando ou os comandos mais eficientes possíveis. Por exemplo,
se você vai persistir um total de 100 objetos Product
e em seguida
chamar o método flush()
, o Doctrine irá criar um único prepared statment
e reutilizá-lo para cada uma das inserções. Esse padrão é chamado de Unit of
Work, e é utilizado porque é rápido e eficiente.
Na hora de criar ou atualizar objetos, o fluxo de trabalho é quase o mesmo. Na
próxima seção, você verá como o Doctrine é inteligente o suficiente para rodar
uma instrução UPDATE
de forma automática se o registro já existir no banco.
Tip
O Doctrine fornece uma biblioteca que permite a você carregar programaticamente dados de teste no seu projeto (i.e. “fixture data”). Para mais informações, veja /bundles/DoctrineFixturesBundle/index.
Trazer um objeto a partir do banco é ainda mais fácil. Por exemplo, suponha
que você tenha configurado uma rota para mostrar um Product
específico
baseado no seu valor id
:
public function showAction($id)
{
$product = $this->getDoctrine()
->getRepository('AcmeStoreBundle:Product')
->find($id);
if (!$product) {
throw $this->createNotFoundException('No product found for id '.$id);
}
// faz algo, como passar o objeto $product para um template
}
Quando você busca um tipo de objeto em particular, você sempre usa o que chamamos de “repositório”. Você pode pensar num repositório como uma classe PHP cuja única função é auxiliar a trazer entidades de uma determinada classe. Você pode acessar o objeto repositório por uma classe entidade dessa forma:
$repository = $this->getDoctrine()
->getRepository('AcmeStoreBundle:Product');
Note
A string AcmeStoreBundle:Product
é um atalho que você pode usar
em qualquer lugar no Doctrine em vez do nome completo da classe entidade
(i.e Acme\StoreBundle\Entity\Product
). Desde que sua entidade esteja
sob o namespace Entity
do seu bundle, isso vai funcionar.
Uma vez que você tiver seu repositório, terá acesso a todos os tipos de métodos úteis:
// Busca pela chave primária (geralmente "id")
$product = $repository->find($id);
// nomes de métodos dinâmicos para busca baseados no valor de uma coluna
$product = $repository->findOneById($id);
$product = $repository->findOneByName('foo');
// busca *todos* os produtos
$products = $repository->findAll();
// busca um grupo de produtos baseada numa valor arbitrário de coluna
$products = $repository->findByPrice(19.99);
Note
Naturalmente, você pode também pode rodar consultas complexas, vamos aprender mais sobre isso na seção Consultando Objetos.
Você também pode se aproveitar dos métodos bem úteis findBy
e
findOneBy
para retornar facilmente objetos baseando-se em múltiplas
condições:
// busca por um produto que corresponda a um nome e um preço
$product = $repository->findOneBy(array('name' => 'foo', 'price' => 19.99));
// busca por todos os produtos correspondentes a um nome, ordenados por
// preço
$product = $repository->findBy(
array('name' => 'foo'),
array('price' => 'ASC')
);
Depois que você trouxe um objeto do Doctrine, a atualização é fácil. Suponha que você tenha uma rota que mapeia o id de um produto para uma action de atualização em um controller:
public function updateAction($id)
{
$em = $this->getDoctrine()->getManager();
$product = $em->getRepository('AcmeStoreBundle:Product')->find($id);
if (!$product) {
throw $this->createNotFoundException('No product found for id '.$id);
}
$product->setName('New product name!');
$em->flush();
return $this->redirect($this->generateUrl('homepage'));
}
Atualizar um objeto envolve apenas três passos:
flush()
no entity managerObserve que não é necessário chamar $em->persist($product)
. Chamar novamente
esse método apenas diz ao Doctrine para gerenciar ou “ficar de olho” no objeto
$product
. Nesse caso, como o objeto $product
foi trazido do Doctrine,
ele já está sendo gerenciado.
Apagar um objeto é muito semelhante, mas requer um chamada ao método
remove()
do entity manager:
$em->remove($product);
$em->flush();
Como você podia esperar, o método remove()
notifica o Doctrine que você
quer remover uma determinada entidade do banco. A consulta real DELETE
, no
entanto, não é executada de verdade até que o método flush()
seja chamado.
Você já viu como o repositório objeto permite que você execute consultas básicas sem nenhum esforço:
$repository->find($id);
$repository->findOneByName('Foo');
É claro, o Doctrine também permite que se escreva consulta mais complexas
usando o Doctrine Query Language (DQL). O DQL é similar ao SQL exceto que você
deve imaginar que você está consultando um ou mais objetos de uma classe entidade
(i.e. Product
) em vez de consultar linhas em uma tabela (i.e. product
).
Quando estiver consultando no Doctrine, você tem duas opções: escrever consultas Doctrine puras ou usar o Doctrine’s Query Builder.
Imagine que você queira buscar por produtos, mas retornar apenas produtos que
custem menos que 19,99
, ordenados do mais barato para o mais caro. De um
controller, faça o seguinte:
$em = $this->getDoctrine()->getManager();
$query = $em->createQuery(
'SELECT p FROM AcmeStoreBundle:Product p WHERE p.price > :price ORDER BY p.price ASC'
)->setParameter('price', '19.99');
$products = $query->getResult();
Se você se sentir confortável com o SQL, então o DQL deve ser bem natural. A
grande diferença é que você precisa pensar em termos de “objetos” em vez de
linhas no banco de dados. Por esse motivo, você faz um “select” from
AcmeStoreBundle:Product
e dá para ele o alias p
.
O método getResult()
retorna um array de resultados. Se você estiver
buscando por apenas um objeto, você pode usar em vez disso o método
getSingleResult()
:
$product = $query->getSingleResult();
Caution
O método getSingleResult()
gera uma exceção
Doctrine\ORM\NoResultException
se nenhum resultado for retornado e uma
Doctrine\ORM\NonUniqueResultException
se mais de um resultado for
retornado. Se você usar esse método, você vai precisar envolvê-lo em um
bloco try-catch e garantir que apenas um resultado é retornado (se estiver
buscando algo que possa de alguma forma retornar mais de um resultado):
$query = $em->createQuery('SELECT ....')
->setMaxResults(1);
try {
$product = $query->getSingleResult();
} catch (\Doctrine\Orm\NoResultException $e) {
$product = null;
}
// ...
A sintaxe DQL é incrivelmente poderosa, permitindo que você faça junções entre entidades facilmente (o tópico de relacionamentos será coberto posteriormente), grupos etc. Para mais informações, veja a documentação oficial do Doctrine Query Language.
Em vez de escrever diretamente suas consultas, você pode alternativamente usar
o QueryBuilder
do Doctrine para fazer o mesmo serviço usando uma bela
interface orientada a objetos. Se você utilizar uma IDE, pode também se
beneficiar do auto-complete à medida que você digita o nome dos métodos. A
partir de um controller:
$repository = $this->getDoctrine()
->getRepository('AcmeStoreBundle:Product');
$query = $repository->createQueryBuilder('p')
->where('p.price > :price')
->setParameter('price', '19.99')
->orderBy('p.price', 'ASC')
->getQuery();
$products = $query->getResult();
O objeto QueryBuilder
contém todos os métodos necessários para criar sua
consulta. Ao chamar o método getQuery(), o query builder retorna um objeto
``Query
normal, que é o mesmo objeto que você criou diretamente na seção
anterior.
Para mais informações, consulte a documentação do Query Builder do Doctrine.
Nas seções anteriores, você começou a construir e usar consultas mais complexas de dentro de um controller. De modo a isolar, testar e reutilizar essas consultas, é uma boa ideia criar uma classe repositório personalizada para sua entidade e adicionar métodos com sua lógica de consultas lá dentro.
Para fazer isso, adicione o nome da classe repositório na sua definição de mapeamento.
1 2 3 4 5 6 7 8 9 10 11 12 | // src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="Acme\StoreBundle\Repository\ProductRepository")
*/
class Product
{
//...
}
|
1 2 3 4 5 | # src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
type: entity
repositoryClass: Acme\StoreBundle\Repository\ProductRepository
# ...
|
1 2 3 4 5 6 7 8 9 | <!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
<!-- ... -->
<doctrine-mapping>
<entity name="Acme\StoreBundle\Entity\Product"
repository-class="Acme\StoreBundle\Repository\ProductRepository">
<!-- ... -->
</entity>
</doctrine-mapping>
|
O Doctrine pode gerar para você a classe repositório usando o mesmo comando utilizado anteriormente para criar os métodos getters e setters que estavam faltando:
1 | php app/console doctrine:generate:entities Acme
|
Em seguida, adicione um novo método - findAllOrderedByName()
- para sua
recém-gerada classe repositório. Esse método irá buscar por todas as
entidades Product
, ordenadas alfabeticamente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // src/Acme/StoreBundle/Repository/ProductRepository.php
namespace Acme\StoreBundle\Repository;
use Doctrine\ORM\EntityRepository;
class ProductRepository extends EntityRepository
{
public function findAllOrderedByName()
{
return $this->getEntityManager()
->createQuery('SELECT p FROM AcmeStoreBundle:Product p ORDER BY p.name ASC')
->getResult();
}
}
|
Tip
O entity manager pode ser acessado via $this->getEntityManager()
de
dentro do repositório.
Você pode usar esse novo método da mesma forma que os métodos padrões “find” do repositório:
$em = $this->getDoctrine()->getManager();
$products = $em->getRepository('AcmeStoreBundle:Product')
->findAllOrderedByName();
Note
Quando estiver usando uma classe repositório personalizada, você continua
tendo acesso aos métodos padrões finder com find()
e findAll()
.
Suponha que todos os produtos na sua aplicação pertençam exatamente a uma
“categoria”. Nesse caso, você precisa de um objeto Category
e de uma forma
de relacionar um objeto Produto
com um objeto Category
. Comece criando
uma entidade Category
. Como você sabe que irá eventualmente precisar de fazer
a persistência da classe através do Doctrine, você pode deixá-lo criar a classe por
você.
1 | php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Category" --fields="name:string(255)"
|
Esse comando gera a entidade Category
para você, com um campo id
, um
campo name
e as funções getters e setters relacionadas.
Para relacionar as entidades Category
e Product
, comece criando a
propriedade products
na classe Category
:
// src/Acme/StoreBundle/Entity/Category.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
class Category
{
// ...
/**
* @ORM\OneToMany(targetEntity="Product", mappedBy="category")
*/
protected $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
}
Primeiro, como o objeto Category
irá se relacionar a vários objetos
Product`, uma propriedade array ``products
é adicionada para guardar esses
objetos Product
. Novamente, isso não é feito porque o Doctrine precisa
dele, mas na verdade porque faz sentido dentro da aplicação guardar um array de
objetos Product
.
Note
O código no método __construct()
é importante porque o Doctrine requer
que a propriedade $products``seja um objeto ``ArrayCollection
. Esse
objeto se parece e age quase exatamente como um array, mas tem mais um
pouco de flexibilidade embutida. Se isso te deixa desconfortável, não se
preocupe. Apenas imagine que ele é um array
e você estará em boas mãos.
Em seguida, como cada classe Product
pode se relacionar exatamente com um
objeto Category
, você irá querer adicionar uma propriedade $category
na
classe Product
:
// src/Acme/StoreBundle/Entity/Product.php
// ...
class Product
{
// ...
/**
* @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
* @ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
protected $category;
}
Finalmente, agora que você adicionou um nova propriedade tanto na classe
Category
quanto na Product
, diga ao Doctrine para gerar os métodos
getters e setters que estão faltando para você:
1 | php app/console doctrine:generate:entities Acme
|
Ignore o metadado do Doctrine por um instante. Agora você tem duas classes -
Category
e Product
com um relacionamento natural um-para-muitos. A
classe categoria contém um array de objetos Product
e o objeto Product
pode conter um objeto Category
. Em outras palavras - você construiu suas
classes de um jeito que faz sentido para as suas necessidades. O fato de que
os dados precisam ser persistidos no banco é sempre secundário.
Agora, olhe o metadado acima da propriedade $category
na classe
Product
. A informação aqui diz para o Doctrine que a classe relacionada é a
Category
e que ela deve guardar o id
do registro categoria em um campo
category_id
que fica na tabela product
. Em outras palavras, o objeto
Category
será guardado na propriedade $category
, mas nos bastidores, o
Doctrine irá persistir esse relacionamento guardando o valor do id da categoria
na coluna category_id
da tabela product
.
O metadado acima da propriedade $products
do objeto Category
é menos
importante, e simplesmente diz ao Doctrine para olhar a propriedade
Product.category
para descobrir como o relacionamento é mapeado.
Antes de continuar, tenha certeza de dizer ao Doctrine para adicionar uma nova
tabela category
, além de uma coluna product.category_id
e uma nova
chave estrangeira:
1 | php app/console doctrine:schema:update --force
|
Note
Esse comando deve ser usado apenas durante o desenvolvimento. Para um método mais robusto de atualização sistemática em um banco de dados de produção, leia sobre as Doctrine migrations.
Agora é o momento de ver o código em ação. Imagine que você está dentro de um controller:
// ...
use Acme\StoreBundle\Entity\Category;
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
// ...
class DefaultController extends Controller
{
public function createProductAction()
{
$category = new Category();
$category->setName('Main Products');
$product = new Product();
$product->setName('Foo');
$product->setPrice(19.99);
// relaciona a categoria com esse produto
$product->setCategory($category);
$em = $this->getDoctrine()->getManager();
$em->persist($category);
$em->persist($product);
$em->flush();
return new Response(
'Created product id: '.$product->getId().' and category id: '.$category->getId()
);
}
}
Agora, um registro único é adicionado para ambas tabelas category
e
product
. A coluna product.category_id
para o novo produto é definida
como o que for definido como id
na nova categoria. O Doctrine gerencia a
persistência desse relacionamento para você.
Quando você precisa pegar objetos associados, seu fluxo de trabalho é parecido
com o que foi feito anteriormente. Primeiro, consulte um objeto $product
e
então acesse seu o objeto Category
relacionado:
public function showAction($id)
{
$product = $this->getDoctrine()
->getRepository('AcmeStoreBundle:Product')
->find($id);
$categoryName = $product->getCategory()->getName();
// ...
}
Nesse exemplo, você primeiro busca por um objeto Product
baseado no id
do produto. Isso gera uma consulta apenas para os dados do produto e faz um
hydrate do objeto $product
com esses dados. Em seguida, quando você chamar
$product->getCategory()->getName()
, o Doctrine silenciosamente faz uma
segunda consulta para buscar a Category
que está relacionada com esse
Product
. Ele prepara o objeto $category
e o retorna para você.
O que é importante é o fato de que você tem acesso fácil as categorias relacionadas com os produtos, mas os dados da categoria não são realmente retornados até que você peça pela categoria (i.e. sofre “lazy load”).
Você também pode buscar na outra direção:
public function showProductAction($id)
{
$category = $this->getDoctrine()
->getRepository('AcmeStoreBundle:Category')
->find($id);
$products = $category->getProducts();
// ...
}
Nesse caso, ocorre a mesma coisa: primeiro você busca por um único objeto
Category
, e então o Doctrine faz uma segunda busca para retornar os objetos
Product
relacionados, mas apenas se você pedir por eles (i.e. quando você
chama ->getProducts()
). A variável $products
é uma array de todos os
objetos Product
que estão relacionados com um dado objeto Category
por
meio do valor de seu campo category_id
.
Nos exemplos acima, duas consultas foram feitas - uma para o objeto original
(e.g uma Category
) e uma para os objetos relacionados (e.g. os objetos
Product
).
Tip
Lembre que você pode visualizar todas as consultas feitas durante uma requisição pela web debug toolbar.
É claro, se você souber antecipadamente que vai precisar acessar ambos os
objetos, você pode evitar a segunda consulta através da emissão de um “join”
na consulta original. Inclua o método seguinte na classe
ProductRepository
:
// src/Acme/StoreBundle/Repository/ProductRepository.php
public function findOneByIdJoinedToCategory($id)
{
$query = $this->getEntityManager()
->createQuery('
SELECT p, c FROM AcmeStoreBundle:Product p
JOIN p.category c
WHERE p.id = :id'
)->setParameter('id', $id);
try {
return $query->getSingleResult();
} catch (\Doctrine\ORM\NoResultException $e) {
return null;
}
}
Agora, você pode usar esse método no seu controller para buscar um objeto
Product
e sua Category
relacionada com apenas um consulta:
public function showAction($id)
{
$product = $this->getDoctrine()
->getRepository('AcmeStoreBundle:Product')
->findOneByIdJoinedToCategory($id);
$category = $product->getCategory();
// ...
}
Essa seção foi uma introdução para um tipo comum de relacionamento de
entidades, o um-para-muitos. Para detalhes mais avançados e exemplos de como
usar outros tipos de relacionamentos (i.e. um-para-um,
``muitos-para-muitos
), verifique a `Documentação sobre Mapeamento e Associações`_ do
Doctrine.
Note
Se você estiver usando annotations, irá precisar prefixar todas elas com
ORM\
(e.g ORM\OneToMany
), o que não está descrito na documentação
do Doctrine. Você também precisará incluir a instrução
use Doctrine\ORM\Mapping as ORM;
, que faz a importação do prefixo
ORM
das annotations.
O Doctrine é altamente configurável, embora você provavelmente não vai precisar se preocupar com a maioria de suas opções. Para saber mais sobre a configuração do Doctrine, veja a seção Doctrine do reference manual.
Às vezes, você precisa executar uma ação justamente antes ou depois de uma entidade ser inserida, atualizada ou apagada. Esses tipos de ações são conhecidas como “lifecycle” callbacks, pois elas são métodos callbacks que você precisa executar durante diferentes estágios do ciclo de vida de uma entidade (i.e. a entidade foi inserida, atualizada, apagada, etc.).
Se você estiver usando annotations para seus metadados, comece habilitando esses callbacks. Isso não é necessário se estiver utilizando YAML ou XML para seus mapeamentos:
1 2 3 4 5 6 7 8 | /**
* @ORM\Entity()
* @ORM\HasLifecycleCallbacks()
*/
class Product
{
// ...
}
|
Agora, você pode dizer ao Doctrine para executar um método em cada um dos
eventos de ciclo de vida disponíveis. Por exemplo, suponha que você queira
definir uma coluna created
do tipo data para a data atual, apenas quando for
a primeira persistência da entidade (i.e. inserção):
1 2 3 4 5 6 7 | /**
* @ORM\prePersist
*/
public function setCreatedValue()
{
$this->created = new \DateTime();
}
|
1 2 3 4 5 6 | # src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
type: entity
# ...
lifecycleCallbacks:
prePersist: [ setCreatedValue ]
|
1 2 3 4 5 6 7 8 9 10 11 | <!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
<!-- ... -->
<doctrine-mapping>
<entity name="Acme\StoreBundle\Entity\Product">
<!-- ... -->
<lifecycle-callbacks>
<lifecycle-callback type="prePersist" method="setCreatedValue" />
</lifecycle-callbacks>
</entity>
</doctrine-mapping>
|
Note
O exemplo acima presume que você tenha criado e mapeado uma propriedade
created
(que não foi mostrada aqui).
Agora, logo no momento anterior a entidade ser persistida pela primeira vez, o
Doctrine irá automaticamente chamar esse método e o campo created
será
preenchido com a data atual.
Isso pode ser repetido para qualquer um dos outros eventos de ciclo de vida, que incluem:
preRemove
postRemove
prePersist
postPersist
preUpdate
postUpdate
postLoad
loadClassMetadata
Para mais informações sobre o que esses eventos significam e sobre os lifecycle callbacks em geral, veja a `Documentação sobre Lifecycle Events`_ do Doctrine.
O Doctrine é bastante flexível, e um grande número de extensões de terceiros está disponível o que permirte que você execute facilmente tarefas repetitivas e comuns nas suas entidades. Isso inclui coisas como Sluggable, Timestampable, Loggable, Translatable e Tree.
Para mais informações sobre como encontrar e usar essas extensões, veja o artigo no cookbook sobre using common Doctrine extensions.
O Doctrine já vem com um grande número de tipos de campo disponível. Cada um deles mapeia um tipo de dados do PHP para um tipo de coluna específico em qualquer banco de dados que você estiver utilizando. Os seguintes tipos são suportados no Doctrine:
string
(usado para strings curtas)text
(usado para strings longas)integer
smallint
bigint
decimal
float
date
time
datetime
boolean
object
(serializado e armazenado em um campo CLOB
)array
(serializado e guardado em um campo CLOB
)Para mais informações, veja a Documentação sobre Tipos de Mapeamento do Doctrine.
Cada campo pode ter um conjunto de opções aplicado sobre ele. As opções
disponíveis incluem type
(o padrão é string
), name
, lenght
,
unique
e nullable
. Olhe alguns exemplos de annotations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /**
* Um campo string com tamanho 255 que não pode ser nulo
* (segue os valores padrões para "type", "length" e *nullable* options)
*
* @ORM\Column()
*/
protected $name;
/**
* Um campo string com tamanho 150 persistido na coluna "email_adress"
* e com um índice único
*
* @ORM\Column(name="email_address", unique="true", length="150")
*/
protected $email;
|
Note
Existem mais algumas opções que não estão listadas aqui. Para mais detalhes, veja a `Documentação sobre Mapeamento de Propriedades`_ do Doctrine.
A integração com o Doctrine2 ORM fornece vários comandos de console no
namespace doctrine
. Para ver a lista de comandos, você pode executar o
console sem nenhum argumento:
1 | php app/console
|
A lista dos comandos disponíveis será mostrada, muitos dos quais começam com o
prefixo doctrine
. Você pode encontrar mais informações sobre qualquer um
desses comandos (e qualquer comando do Symfony) rodando o comando help
.
Por exemplo, para pegar detalhes sobre o comando doctrine:database:create
,
execute:
1 | php app/console help doctrine:database:create
|
Alguns comandos interessantes e notáveis incluem:
doctrine:ensure-production-settings
- verifica se o ambiente atual está
configurado de forma eficiente para produção. Deve ser sempre executado no
ambiente prod
:
1 | php app/console doctrine:ensure-production-settings --env=prod
|
doctrine:mapping:import
- permite ao Doctrine fazer introspecção de um
banco de dados existente e criar a informação de mapeamento. Para mais
informações veja /cookbook/doctrine/reverse_engineering.
doctrine:mapping:info
- diz para você todas as entidades que o Doctrine
tem conhecimento e se existe ou não algum erro básico com o mapeamento.
doctrine:query:dql
and doctrine:query:sql
- permite que você execute
consultas DQL ou SQL diretamente na linha de comando.
Note
Para poder carregar data fixtures para seu banco de dados, você precisa ter
o bundle DoctrineFixturesBundle
instalado. Para aprender como fazer
isso, leia a entrada “/bundles/DoctrineFixturesBundle/index” da
documentação.
Com o Doctrine, você pode se focar nos seus objetos e como eles podem ser úteis na sua aplicação, deixando a preocupação com a persistência de banco de dados em segundo plano. Isso porque o Doctrine permite que você use qualquer objeto PHP para guardar seus dados e se baseia nos metadados de mapeamento para mapear os dados de um objetos para um tabela específica no banco.
E apesar do Doctrine girar em torno de um conceito simples, ele é incrivelmente poderoso, permitindo que você crie consultas complexas e faça subscrição em eventos que permitem a você executar ações diferentes à medida que os objetos vão passando pelo seu ciclo de vida de persistência.