Entidades, repositórios e EntityManager
1. Introdução ao Doctrine ORM e seus Componentes Centrais
O Doctrine ORM é o mapeador objeto-relacional mais maduro do ecossistema PHP, permitindo que desenvolvedores interajam com bancos de dados relacionais utilizando objetos puros. Diferentemente do PDO, que exige consultas SQL manuais e hidratação manual de dados, o Doctrine abstrai completamente a camada de persistência, transformando operações de banco em manipulações diretas de objetos PHP.
Os três pilares fundamentais do Doctrine são:
- Entidades: Classes PHP que representam tabelas do banco de dados
- Repositórios: Classes responsáveis por consultas e recuperação de entidades
- EntityManager: O gerenciador central que orquestra o ciclo de vida das entidades
Enquanto com PDO você escreveria $stmt = $pdo->query("SELECT * FROM usuarios WHERE id = 1"); $usuario = $stmt->fetch();, com Doctrine você simplesmente faz $usuario = $entityManager->find(Usuario::class, 1); e recebe um objeto totalmente funcional.
2. Mapeamento de Entidades com Anotações
O mapeamento define como uma classe PHP se relaciona com uma tabela do banco. Utilizamos atributos PHP 8+ para configurar esse mapeamento:
<?php
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: "clientes")]
class Cliente
{
#[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: 150, unique: true)]
private string $email;
#[ORM\OneToMany(targetEntity: Pedido::class, mappedBy: "cliente")]
private $pedidos;
// Getters e setters omitidos para brevidade
}
Relacionamentos são mapeados diretamente:
<?php
#[ORM\Entity]
#[ORM\Table(name: "pedidos")]
class Pedido
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: "integer")]
private int $id;
#[ORM\Column(type: "datetime")]
private \DateTime $dataCriacao;
#[ORM\ManyToOne(targetEntity: Cliente::class, inversedBy: "pedidos")]
#[ORM\JoinColumn(name: "cliente_id", referencedColumnName: "id")]
private Cliente $cliente;
#[ORM\OneToMany(targetEntity: ItemPedido::class, mappedBy: "pedido", cascade: ["persist", "remove"])]
private $itens;
}
3. Ciclo de Vida de uma Entidade e o EntityManager
Uma entidade passa por quatro estados principais durante sua existência:
- New: Objeto criado com
new, mas não gerenciado pelo EntityManager - Managed: Objeto registrado via
persist()e sincronizado com o banco - Detached: Objeto que foi gerenciado, mas não está mais sob controle do EntityManager
- Removed: Objeto marcado para exclusão via
remove()
<?php
// Novo objeto - estado NEW
$cliente = new Cliente();
$cliente->setNome('Maria Silva');
$cliente->setEmail('maria@exemplo.com');
// Persistindo - estado MANAGED
$entityManager->persist($cliente);
// Forçando sincronização com o banco
$entityManager->flush();
// Removendo - estado REMOVED
$entityManager->remove($cliente);
$entityManager->flush();
// Gerenciamento de transações
$entityManager->beginTransaction();
try {
$entityManager->persist($cliente);
$entityManager->flush();
$entityManager->commit();
} catch (\Exception $e) {
$entityManager->rollback();
throw $e;
}
O método find() é a forma mais comum de recuperar entidades gerenciadas:
<?php
$cliente = $entityManager->find(Cliente::class, 1);
// Agora $cliente está no estado MANAGED
4. Repositórios Customizados e Consultas
O Doctrine oferece métodos mágicos nos repositórios padrão, mas para consultas complexas criamos repositórios customizados:
<?php
use Doctrine\ORM\EntityRepository;
class PedidoRepository extends EntityRepository
{
public function findPedidosPorPeriodo(\DateTime $inicio, \DateTime $fim): array
{
$dql = "SELECT p FROM App\Entity\Pedido p
WHERE p.dataCriacao BETWEEN :inicio AND :fim
ORDER BY p.dataCriacao DESC";
return $this->getEntityManager()
->createQuery($dql)
->setParameter('inicio', $inicio)
->setParameter('fim', $fim)
->getResult();
}
public function findPedidosComItens(int $clienteId): array
{
$qb = $this->createQueryBuilder('p');
return $qb->join('p.itens', 'i')
->where('p.cliente = :clienteId')
->setParameter('clienteId', $clienteId)
->getQuery()
->getResult();
}
}
Métodos mágicos do repositório padrão:
<?php
$pedidos = $repository->findBy(['status' => 'pendente'], ['dataCriacao' => 'DESC']);
$pedido = $repository->findOneBy(['id' => 1]);
$todos = $repository->findAll();
5. Gerenciamento de Associações e Identidade
O mapeamento de chaves estrangeiras é transparente no Doctrine. Ao definir um ManyToOne, o Doctrine gerencia automaticamente a coluna de chave estrangeira:
<?php
// Criando um pedido associado a um cliente existente
$cliente = $entityManager->find(Cliente::class, 1);
$pedido = new Pedido();
$pedido->setCliente($cliente); // Doctrine insere automaticamente cliente_id
$pedido->setDataCriacao(new \DateTime());
$item = new ItemPedido();
$item->setProduto('Notebook');
$item->setQuantidade(1);
$item->setPreco(4500.00);
$item->setPedido($pedido);
$pedido->getItens()->add($item);
// Com cascade={"persist"}, persistir o pedido persiste também os itens
$entityManager->persist($pedido);
$entityManager->flush();
O UnitOfWork do Doctrine mantém um cache de identidade, garantindo que uma mesma linha do banco seja representada pelo mesmo objeto em memória:
<?php
$pedido1 = $entityManager->find(Pedido::class, 1);
$pedido2 = $entityManager->find(Pedido::class, 1);
var_dump($pedido1 === $pedido2); // true - mesma instância
6. Boas Práticas e Padrões com EntityManager
A injeção de dependência do EntityManager deve ser feita através de serviços, nunca diretamente em controllers:
<?php
class PedidoService
{
private EntityManagerInterface $entityManager;
private PedidoRepository $pedidoRepository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
$this->pedidoRepository = $entityManager->getRepository(Pedido::class);
}
public function criarPedido(array $dados): Pedido
{
$this->entityManager->beginTransaction();
try {
$pedido = new Pedido();
// Configuração do pedido...
$this->entityManager->persist($pedido);
$this->entityManager->flush();
$this->entityManager->commit();
return $pedido;
} catch (\Exception $e) {
$this->entityManager->rollback();
throw $e;
}
}
public function processarLote(array $pedidos): void
{
$batchSize = 20;
$i = 0;
foreach ($pedidos as $pedido) {
$this->entityManager->persist($pedido);
$i++;
if ($i % $batchSize === 0) {
$this->entityManager->flush();
$this->entityManager->clear(); // Evita memory leak
}
}
$this->entityManager->flush();
}
}
Evite problemas comuns como detached entities ao serializar objetos:
<?php
// Problema: entidade detached após serialização
session_start();
$_SESSION['pedido'] = $pedido; // Perde conexão com EntityManager
// Solução: recarregar a entidade
$pedido = $entityManager->find(Pedido::class, $_SESSION['pedido_id']);
7. Exemplo Prático Completo: Sistema de Pedidos
Vamos implementar um fluxo completo com as entidades Cliente, Pedido e ItemPedido:
<?php
// 1. Criando cliente
$cliente = new Cliente();
$cliente->setNome('João Santos');
$cliente->setEmail('joao@exemplo.com');
$entityManager->persist($cliente);
$entityManager->flush();
// 2. Criando pedido com itens
$pedido = new Pedido();
$pedido->setCliente($cliente);
$pedido->setDataCriacao(new \DateTime());
$item1 = new ItemPedido();
$item1->setProduto('Smartphone');
$item1->setQuantidade(2);
$item1->setPreco(2500.00);
$item1->setPedido($pedido);
$item2 = new ItemPedido();
$item2->setProduto('Capa Protetora');
$item2->setQuantidade(2);
$item2->setPreco(50.00);
$item2->setPedido($pedido);
$pedido->getItens()->add($item1);
$pedido->getItens()->add($item2);
$entityManager->persist($pedido);
$entityManager->flush();
// 3. Consultando pedidos por período
$inicio = new \DateTime('2024-01-01');
$fim = new \DateTime('2024-12-31');
$pedidosPeriodo = $pedidoRepository->findPedidosPorPeriodo($inicio, $fim);
// 4. Removendo um pedido (cascade remove itens automaticamente)
$pedidoParaRemover = $entityManager->find(Pedido::class, 1);
$entityManager->remove($pedidoParaRemover);
$entityManager->flush();
echo "Sistema de pedidos executado com sucesso!";
Este exemplo demonstra o poder do Doctrine ORM: operações complexas de banco são reduzidas a simples manipulações de objetos, enquanto o EntityManager gerencia todo o ciclo de vida, transações e consistência dos dados automaticamente.
Referências
- Documentação Oficial do Doctrine ORM — Guia completo de todos os recursos do Doctrine, incluindo mapeamento de entidades, EntityManager e repositórios
- Doctrine ORM: Entidades e Mapeamento — Tutorial detalhado sobre mapeamento básico de entidades com atributos e anotações
- Doctrine ORM: Ciclo de Vida e Eventos — Referência completa sobre eventos do ciclo de vida e gerenciamento de entidades
- Symfony Doctrine Documentation — Guia prático de uso do Doctrine no ecossistema Symfony, com exemplos de repositórios e EntityManager
- Doctrine ORM: Repositórios Customizados — Documentação oficial sobre criação e uso de repositórios customizados com DQL e QueryBuilder
- PHP The Right Way: Databases and ORM — Seção sobre boas práticas com ORM em PHP, incluindo Doctrine
- Doctrine ORM: Unit of Work e Identity Map — Explicação detalhada do padrão Unit of Work e cache de identidade no Doctrine