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