.. index::
single: Doctrine
Bancos de Dados e Doctrine
==========================
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
":doc:`/cookbook/doctrine/dbal`".
Você também pode persistir dados no `MongoDB`_ usando a biblioteca Doctrine
ODM. Para mais informações, leia a documentação
":doc:`/bundles/DoctrineMongoDBBundle/index`".
Um Exemplo Simples: Um Produto
------------------------------
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.
.. sidebar:: Codifique seguindo o exemplo
Se quiser seguir o exemplo deste capítulo, crie um ``AcmeStoreBundle`` via:
.. code-block:: bash
php app/console generate:bundle --namespace=Acme/StoreBundle
Configurando o Banco de Dados
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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``:
.. code-block:: yaml
# 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:
.. code-block:: yaml
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 :doc:`/cookbook/configuration/external_parameters`.
Agora que o Doctrine sabe sobre seu banco, pode deixar que ele faça a criação
dele para você:
.. code-block:: bash
php app/console doctrine:database:create
Criando uma Classe Entidade
~~~~~~~~~~~~~~~~~~~~~~~~~~~
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ê:
.. code-block:: bash
php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Product" --fields="name:string(255) price:float description:text"
.. index::
single: Doctrine; Adding mapping metadata
.. _book-doctrine-adding-mapping:
Adicionando Informações de Mapeamento
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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:
.. image:: /images/book/doctrine_image_1.png
:align: center
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.
.. configuration-block::
.. code-block:: php-annotations
// 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;
}
.. code-block:: yaml
# 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
.. code-block:: xml
.. 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
:ref:`book-doctrine-field-types`.
.. seealso::
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
Gerando os Getters e Setters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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:
.. code-block:: bash
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.
.. code-block:: bash
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.
Criando as Tabelas/Esquema do Banco de Dados
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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:
.. code-block:: bash
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 :doc:`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.
Persistindo Objetos no Banco de Dados
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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:
.. code-block:: php
:linenos:
// 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:
* **linhas 8-11** Nessa parte você instancia o objeto ``$product`` como
qualquer outro objeto PHP normal;
* **linha 13** Essa linha recuperar o objeto *entity manager* do Doctrine, que
é o responsável por lidar com o processo de persistir e retornar objetos do
e para o banco de dados;
* **linha 14** O método ``persist()`` diz ao Doctrine para ''gerenciar'' o
objeto ``$product``. Isso não gera (ainda) um comando real no banco de dados.
* **linha 15** Quando o método ``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 :doc:`/bundles/DoctrineFixturesBundle/index`.
Trazendo Objetos do Banco de Dados
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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 :ref:`book-doctrine-queries`.
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')
);
.. tip::
Quando você renderiza uma página, você pode ver quantas buscas foram feitas
no canto inferior direito da web debug toolbar.
.. image:: /images/book/doctrine_web_debug_toolbar.png
:align: center
:scale: 50
:width: 350
Se você clicar no ícone, irá abrir o profiler, mostrando a você as
consultas exatas que foram feitas.
Atualizando um Objeto
~~~~~~~~~~~~~~~~~~~~~
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:
1. retornar um objeto do Doctrine;
2. modificar o objeto;
3. chamar ``flush()`` no entity manager
Observe 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.
Excluindo um Objeto
~~~~~~~~~~~~~~~~~~~
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.
.. _`book-doctrine-queries`:
Consultando Objetos
-------------------
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.
Consultando Objetos com DQL
~~~~~~~~~~~~~~~~~~~~~~~~~~~
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
:ref:`relacionamentos` será coberto posteriormente),
grupos etc. Para mais informações, veja a documentação oficial do
`Doctrine Query Language`_.
.. sidebar:: Configurando parâmetros
Tome nota do método ``setParameter()``. Quando trabalhar com o Doctrine,
é sempre uma boa ideia configurar os valores externos como
``placeholders``, o que foi feito na consulta acima:
.. code-block:: text
... WHERE p.price > :price ...
Você pode definir o valor do placeholder ``price``chamando o método
``setParameter()``::
->setParameter('price', '19.99')
Usar parâmetros em vez de colocar os valores diretamente no texto da
consulta é feito para prevenir ataques de SQL injection e deve ser feito
*sempre*. Se você estiver usando múltiplos parâmetros, você pode definir seus
valores de uma vez só usando o método ``setParameters()``::
->setParameters(array(
'price' => '19.99',
'name' => 'Foo',
))
Usando o Doctrine's Query Builder
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
Classes Repositório Personalizadas
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
.. configuration-block::
.. code-block:: php-annotations
// 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
{
//...
}
.. code-block:: yaml
# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
type: entity
repositoryClass: Acme\StoreBundle\Repository\ProductRepository
# ...
.. code-block:: xml
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:
.. code-block:: bash
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.
.. code-block:: php
// 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()``.
.. _`book-doctrine-relations`:
Relacionamentos/Associações de Entidades
----------------------------------------
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ê.
.. code-block:: bash
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.
Metadado para Mapeamento de Relacionamentos
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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ê:
.. code-block:: bash
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``.
.. image:: /images/book/doctrine_image_2.png
:align: center
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:
.. code-block:: bash
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
:doc:`Doctrine migrations`.
Salvando as Entidades Relacionadas
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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ê.
Retornando Objetos Relacionados
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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ê.
.. image:: /images/book/doctrine_image_3.png
:align: center
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``.
.. sidebar:: Relacionamentos e Classes Proxy
O "lazy loading" é possível porque, quando necessário, o Doctrine retorna
um objeto "proxy" no lugar do objeto real. Olhe novamente o exemplo acima::
$product = $this->getDoctrine()
->getRepository('AcmeStoreBundle:Product')
->find($id);
$category = $product->getCategory();
// prints "Proxies\AcmeStoreBundleEntityCategoryProxy"
echo get_class($category);
Esse objeto proxy estende o verdadeiro objeto ``Category``, e se parece e
age exatamente como ele. A diferença é que, por usar um objeto proxy,
o Doctrine pode retardar a busca pelos dados reais da ``Category``até que
você realmente precise daqueles dados (e.g. até que você chame
``$category->getName()``).
As classes proxy são criadas pelo Doctrine e armazenadas no diretório
cache. E apesar de que você provavelmente nunca irá notar que o seu objeto
``$category`` é na verdade um objeto proxy, é importante manter isso em
mente.
Na próxima seção, quando você retorna os dados do produto e categoria todos
de uma vez (via um *join*), o Doctrine irá retornar o *verdadeiro* objeto
``Category`, uma vez que nada precisa ser carregado de modo "lazy load".
Juntando Registros Relacionados
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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();
// ...
}
Mais Informações sobre Associações
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
Configuração
------------
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
:doc:`reference manual`.
Lifecycle Callbacks
-------------------
À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:
.. code-block:: php-annotations
/**
* @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):
.. configuration-block::
.. code-block:: php-annotations
/**
* @ORM\prePersist
*/
public function setCreatedValue()
{
$this->created = new \DateTime();
}
.. code-block:: yaml
# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
type: entity
# ...
lifecycleCallbacks:
prePersist: [ setCreatedValue ]
.. code-block:: xml
.. 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.
.. sidebar:: Lifecycle Callbacks e Event Listeners
Observe que o método ``setCreatedValue()`` não recebe nenhum argumento.
Esse é o comportamento usual dos lifecycle callbacks e é intencional: eles
devem ser métodos simples que estão preocupados com as transformações
internas dos dados na entidade (e.g. preencher um campo created/updated ou
gerar um valor slug).
Se você precisar fazer algo mais pesado - como rotinas de log ou mandar um
e-mail - você deve registrar uma classe externa como um event listener ou
subscriber e dar para ele acesso aos recursos que precisar. Para mais
informações, veja :doc:`/cookbook/doctrine/event_listeners_subscribers`.
Extensões do Doctrine: Timestampable, Sluggable, etc.
-----------------------------------------------------
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
:doc:`using common Doctrine extensions`.
.. _book-doctrine-field-types:
Referência dos Tipos de Campos do Doctrine
------------------------------------------
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:
* **Strings**
* ``string`` (usado para strings curtas)
* ``text`` (usado para strings longas)
* **Números**
* ``integer``
* ``smallint``
* ``bigint``
* ``decimal``
* ``float``
* **Datas e Horários** (usa um objeto `DateTime`_ para esses campos no PHP)
* ``date``
* ``time``
* ``datetime``
* **Outros Tipos**
* ``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.
Opções de Campo
~~~~~~~~~~~~~~~
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:
.. code-block:: php-annotations
/**
* 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.
.. index::
single: Doctrine; ORM Console Commands
single: CLI; Doctrine ORM
Comandos de Console
-------------------
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:
.. code-block:: bash
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:
.. code-block:: bash
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``:
.. code-block:: bash
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 :doc:`/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 ":doc:`/bundles/DoctrineFixturesBundle/index`" da
documentação.
Sumário
-------
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.
Para mais informações sobre o Doctrine, veja a seção *Doctrine* do
:doc:`cookbook`, que inclui os seguintes artigos:
* :doc:`/bundles/DoctrineFixturesBundle/index`
* :doc:`/cookbook/doctrine/common_extensions`
.. _`Doctrine`: http://www.doctrine-project.org/
.. _`MongoDB`: http://www.mongodb.org/
.. _`Documentação Básica sobre Mapeamento do Doctrine`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html
.. _`Query Builder`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/query-builder.html
.. _`Doctrine Query Language`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html
.. _`Documentação sobre Mapeamento e Associações`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html
.. _`DateTime`: http://php.net/manual/en/class.datetime.php
.. _`Documentação sobre Tipos de Mapeamento`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#doctrine-mapping-types
.. _`Documentação sobre Mapeamento de Propriedades`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#property-mapping
.. _`Documentação sobre Lifecycle Events`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events
.. _`Documentação sobre os nomes de comandos SQL reservados`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#quoting-reserved-words
.. _`Classes persistentes`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#persistent-classes
.. _`Mapeamento de propriedades`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#property-mapping