Documentação do Symfony - versão 3.1
Renderizada do repositório symfony-docs-pt-BR no Github

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 “/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”.

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.

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:

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

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ê:

1
php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Product" --fields="name:string(255) price:float description:text"

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:

images/book/doctrine_image_1.png

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.

  • Annotations
     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;
    }
    
  • YAML
     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
    
  • XML
     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

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:

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.

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:

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.

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:

 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:

  • 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 /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 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')
);

Tip

Quando você renderiza uma página, você pode ver quantas buscas foram feitas no canto inferior direito da web debug toolbar.

images/book/doctrine_web_debug_toolbar.png

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.

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 relacionamentos será coberto posteriormente), grupos etc. Para mais informações, veja a documentação oficial do Doctrine Query Language.

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.

  • Annotations
     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
    {
        //...
    }
    
  • YAML
    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
        # ...
    
  • XML
    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().

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ê.

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.

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ê:

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.

images/book/doctrine_image_2.png

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.

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ê.

images/book/doctrine_image_3.png

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.

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 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:

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):

  • Annotations
    1
    2
    3
    4
    5
    6
    7
    /**
     * @ORM\prePersist
     */
    public function setCreatedValue()
    {
        $this->created = new \DateTime();
    }
    
  • YAML
    1
    2
    3
    4
    5
    6
    # src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
    Acme\StoreBundle\Entity\Product:
        type: entity
        # ...
        lifecycleCallbacks:
            prePersist: [ setCreatedValue ]
    
  • XML
     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.

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 using common Doctrine extensions.

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:

 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.

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:

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.

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.