Introdução ao Doctrine ORM
1. O que é o Doctrine ORM e por que usá-lo?
Tradicionalmente, ao trabalhar com bancos de dados em PHP, utilizamos SQL puro ou PDO para executar consultas. Essa abordagem, embora funcional, apresenta diversos problemas à medida que o projeto cresce:
- Código repetitivo: tarefas comuns como CRUD exigem consultas SQL manuais e repetitivas
- Acoplamento ao banco: mudar de SGBD (ex: MySQL para PostgreSQL) exige reescrever consultas
- Falta de orientação a objetos: trabalhamos com arrays associativos, perdendo os benefícios da POO
- Manutenção complexa: consultas espalhadas por toda a aplicação dificultam alterações
O Doctrine ORM (Object-Relational Mapping) resolve esses problemas ao mapear tabelas do banco de dados para classes PHP, permitindo que você trabalhe com objetos em vez de SQL. Ele implementa o padrão Data Mapper, onde uma camada intermediária (EntityManager) gerencia a persistência dos objetos.
Principais vantagens:
- Produtividade: operações comuns são feitas com poucas linhas de código
- Manutenibilidade: regras de negócio ficam centralizadas nas entidades
- Independência de banco: abstrai diferenças entre SGBDs
- Recursos avançados: cache, lazy loading, identity map, unit of work
A arquitetura do Doctrine é composta por:
- EntityManager: ponto central de acesso ao ORM
- Repositories: classes responsáveis por consultas personalizadas
- Unit of Work: rastreia mudanças nas entidades durante uma requisição
2. Instalação e configuração inicial
Para instalar o Doctrine ORM via Composer, execute:
composer require doctrine/orm doctrine/dbal symfony/cache
Agora, crie o arquivo bootstrap.php para configurar a conexão:
<?php
// bootstrap.php
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMSetup;
require_once __DIR__ . '/vendor/autoload.php';
// Configuração dos diretórios de entidades
$paths = [__DIR__ . '/src/Entity'];
$isDevMode = true;
// Configuração do cache (usando array para desenvolvimento)
$cache = new Doctrine\Common\Cache\ArrayCache();
// Configuração do ORM
$config = ORMSetup::createAttributeMetadataConfiguration(
$paths,
$isDevMode,
null,
$cache
);
// Configuração da conexão com banco (exemplo com SQLite)
$connectionParams = [
'driver' => 'pdo_sqlite',
'path' => __DIR__ . '/data/database.sqlite',
];
// Para MySQL, use:
// $connectionParams = [
// 'driver' => 'pdo_mysql',
// 'host' => 'localhost',
// 'dbname' => 'meu_banco',
// 'user' => 'root',
// 'password' => 'senha',
// ];
// Criação do EntityManager
$entityManager = EntityManager::create($connectionParams, $config);
Para usar as ferramentas de linha de comando, crie o arquivo cli-config.php:
<?php
// cli-config.php
require_once 'bootstrap.php';
return Doctrine\ORM\Tools\Console\ConsoleRunner::createHelperSet($entityManager);
3. Mapeamento de entidades (Entity Mapping)
Vamos criar nossa primeira entidade utilizando PHP 8 attributes (recomendado):
<?php
// src/Entity/Usuario.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'usuarios')]
class Usuario
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string', length: 100)]
private string $nome;
#[ORM\Column(type: 'string', length: 180, unique: true)]
private string $email;
#[ORM\Column(type: 'datetime', nullable: true)]
private ?\DateTimeInterface $dataNascimento = null;
#[ORM\Column(type: 'boolean', options: ['default' => true])]
private bool $ativo = true;
// Getters e Setters
public function getId(): int { return $this->id; }
public function getNome(): string { return $this->nome; }
public function setNome(string $nome): self { $this->nome = $nome; return $this; }
public function getEmail(): string { return $this->email; }
public function setEmail(string $email): self { $this->email = $email; return $this; }
public function getDataNascimento(): ?\DateTimeInterface { return $this->dataNascimento; }
public function setDataNascimento(?\DateTimeInterface $dataNascimento): self { $this->dataNascimento = $dataNascimento; return $this; }
public function isAtivo(): bool { return $this->ativo; }
public function setAtivo(bool $ativo): self { $this->ativo = $ativo; return $this; }
}
Para gerar as tabelas no banco, execute:
php vendor/bin/doctrine orm:schema-tool:create
4. Relacionamentos entre entidades
OneToOne (Usuário e Perfil)
// src/Entity/Perfil.php
#[ORM\Entity]
class Perfil
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'text')]
private string $biografia;
#[ORM\OneToOne(inversedBy: 'perfil')]
#[ORM\JoinColumn(nullable: false)]
private Usuario $usuario;
}
Na entidade Usuario, adicione:
#[ORM\OneToOne(mappedBy: 'usuario', cascade: ['persist', 'remove'])]
private ?Perfil $perfil = null;
OneToMany / ManyToOne (Categoria e Produto)
// src/Entity/Categoria.php
#[ORM\Entity]
class Categoria
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string', length: 100)]
private string $nome;
#[ORM\OneToMany(mappedBy: 'categoria', targetEntity: Produto::class, cascade: ['persist'])]
private \Doctrine\Common\Collections\Collection $produtos;
public function __construct()
{
$this->produtos = new \Doctrine\Common\Collections\ArrayCollection();
}
}
// src/Entity/Produto.php
#[ORM\Entity]
class Produto
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string', length: 150)]
private string $nome;
#[ORM\ManyToOne(inversedBy: 'produtos')]
#[ORM\JoinColumn(nullable: false)]
private Categoria $categoria;
}
ManyToMany (Aluno e Curso)
// src/Entity/Aluno.php
#[ORM\Entity]
class Aluno
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string', length: 100)]
private string $nome;
#[ORM\ManyToMany(targetEntity: Curso::class, inversedBy: 'alunos')]
#[ORM\JoinTable(name: 'alunos_cursos')]
private \Doctrine\Common\Collections\Collection $cursos;
public function __construct()
{
$this->cursos = new \Doctrine\Common\Collections\ArrayCollection();
}
}
// src/Entity/Curso.php
#[ORM\Entity]
class Curso
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string', length: 150)]
private string $titulo;
#[ORM\ManyToMany(targetEntity: Aluno::class, mappedBy: 'cursos')]
private \Doctrine\Common\Collections\Collection $alunos;
public function __construct()
{
$this->alunos = new \Doctrine\Common\Collections\ArrayCollection();
}
}
5. EntityManager e o ciclo de vida das entidades
O EntityManager gerencia o ciclo de vida das entidades através do padrão Unit of Work:
<?php
require_once 'bootstrap.php';
use App\Entity\Usuario;
$em = require 'bootstrap.php';
// Criando e persistindo um novo usuário
$usuario = new Usuario();
$usuario->setNome('João Silva');
$usuario->setEmail('joao@email.com');
$usuario->setDataNascimento(new \DateTime('1990-05-15'));
$em->persist($usuario); // Apenas agenda para inserção
$em->flush(); // Executa todas as operações pendentes
echo "Usuário criado com ID: " . $usuario->getId() . PHP_EOL;
// Recuperando entidades
$usuario = $em->find(Usuario::class, 1);
$usuarios = $em->getRepository(Usuario::class)->findBy(['ativo' => true]);
$usuario = $em->getRepository(Usuario::class)->findOneBy(['email' => 'joao@email.com']);
// Atualizando uma entidade
$usuario->setNome('João Santos');
$em->flush(); // Detecta mudanças automaticamente
// Removendo uma entidade
$em->remove($usuario);
$em->flush();
O Identity Map garante que cada entidade seja carregada apenas uma vez por requisição:
$usuario1 = $em->find(Usuario::class, 1);
$usuario2 = $em->find(Usuario::class, 1);
var_dump($usuario1 === $usuario2); // true - mesmo objeto
6. Repositórios (Repositories)
Repositórios centralizam consultas personalizadas:
<?php
// src/Repository/UsuarioRepository.php
namespace App\Repository;
use App\Entity\Usuario;
use Doctrine\ORM\EntityRepository;
class UsuarioRepository extends EntityRepository
{
public function findAtivosComPerfil(): array
{
return $this->createQueryBuilder('u')
->innerJoin('u.perfil', 'p')
->where('u.ativo = :ativo')
->setParameter('ativo', true)
->getQuery()
->getResult();
}
public function countByMes(int $ano): array
{
return $this->createQueryBuilder('u')
->select('MONTH(u.dataCriacao) as mes, COUNT(u.id) as total')
->where('YEAR(u.dataCriacao) = :ano')
->setParameter('ano', $ano)
->groupBy('mes')
->orderBy('mes')
->getQuery()
->getScalarResult();
}
}
Para usar o repositório customizado, registre no mapeamento:
#[ORM\Entity(repositoryClass: UsuarioRepository::class)]
class Usuario { ... }
7. Consultas com DQL e QueryBuilder
DQL (Doctrine Query Language) é uma linguagem de consulta orientada a objetos:
// DQL básica
$dql = "SELECT u FROM App\Entity\Usuario u WHERE u.ativo = :ativo ORDER BY u.nome ASC";
$query = $em->createQuery($dql);
$query->setParameter('ativo', true);
$usuarios = $query->getResult();
// DQL com JOIN
$dql = "SELECT u, p FROM App\Entity\Usuario u JOIN u.perfil p WHERE u.ativo = :ativo";
$usuarios = $em->createQuery($dql)
->setParameter('ativo', true)
->getResult();
// DQL com funções agregadas
$dql = "SELECT c.nome, COUNT(p.id) as total FROM App\Entity\Categoria c JOIN c.produtos p GROUP BY c.id";
$resultados = $em->createQuery($dql)->getScalarResult();
QueryBuilder oferece uma API fluente para construir consultas dinâmicas:
$qb = $em->createQueryBuilder();
$qb->select('u')
->from(Usuario::class, 'u')
->leftJoin('u.perfil', 'p')
->where($qb->expr()->andX(
$qb->expr()->eq('u.ativo', ':ativo'),
$qb->expr()->orX(
$qb->expr()->like('u.nome', ':busca'),
$qb->expr()->like('u.email', ':busca')
)
))
->setParameter('ativo', true)
->setParameter('busca', '%' . $termo . '%')
->orderBy('u.nome', 'ASC')
->setMaxResults(10);
$usuarios = $qb->getQuery()->getResult();
Hidratação de resultados:
// Array de objetos (padrão)
$usuarios = $query->getResult(); // array de Usuario
// Array escalar
$dados = $query->getScalarResult(); // array de arrays simples
// Array simples de uma coluna
$nomes = $query->getSingleColumnResult(); // array de strings
// Um único resultado
$usuario = $query->getOneOrNullResult(); // Usuario ou null
8. Considerações finais e boas práticas
Migrations com Doctrine Migrations:
composer require doctrine/migrations
Crie e execute migrations para versionar o schema do banco:
php vendor/bin/doctrine migrations:diff
php vendor/bin/doctrine migrations:migrate
Performance: evitar N+1 queries:
// Ruim - N+1 queries
$categorias = $em->getRepository(Categoria::class)->findAll();
foreach ($categorias as $categoria) {
foreach ($categoria->getProdutos() as $produto) { // Nova query a cada iteração
echo $produto->getNome();
}
}
// Bom - eager loading com JOIN
$categorias = $em->createQueryBuilder()
->select('c, p')
->from(Categoria::class, 'c')
->leftJoin('c.produtos', 'p')
->getQuery()
->getResult();
Validação de dados: valide antes de persistir:
if (empty($usuario->getNome())) {
throw new \InvalidArgumentException('Nome é obrigatório');
}
if (!filter_var($usuario->getEmail(), FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Email inválido');
}
$em->persist($usuario);
$em->flush();
Organização de código:
- Mantenha entidades anêmicas (apenas getters/setters)
- Coloque lógica de negócio em serviços ou Domain Services
- Use repositórios para consultas complexas
- Evite chamar flush() dentro de loops; chame uma vez no final
O Doctrine ORM é uma ferramenta poderosa que, quando bem utilizada, acelera significativamente o desenvolvimento e melhora a qualidade do código. Comece com projetos pequenos, entenda o ciclo de vida das entidades e, gradualmente, explore recursos avançados como eventos de ciclo de vida, filtros e extensões.
Referências
- Documentação Oficial do Doctrine ORM — Guia completo de referência com todos os recursos do ORM
- Doctrine ORM Getting Started — Tutorial passo a passo para iniciantes com exemplos práticos
- Doctrine Query Builder Documentation — Referência detalhada sobre QueryBuilder e DQL
- Doctrine Migrations Documentation — Documentação oficial para gerenciamento de migrations
- PHP The Right Way - Databases and ORM — Seção sobre bancos de dados e ORM no guia de boas práticas PHP
- Symfony Doctrine ORM Guide — Guia prático de Doctrine no ecossistema Symfony (aplicável a projetos puros)
- Doctrine ORM Best Practices — Boas práticas oficiais para uso do Doctrine em produção