Repository pattern em PHP
1. Introdução ao Repository Pattern
O Repository Pattern é um padrão de design que atua como uma camada de abstração entre a lógica de negócio e a persistência de dados. Seu propósito principal é isolar o domínio da aplicação dos detalhes de implementação do armazenamento, seja ele um banco relacional, um cache, uma API externa ou até mesmo um arquivo.
Os problemas que o Repository resolve são significativos:
- Acoplamento com ORM/banco: Sem ele, o código de negócio frequentemente contém chamadas diretas ao Eloquent, Doctrine ou PDO, dificultando mudanças futuras.
- Testabilidade: Repositories permitem substituir a implementação real por mocks durante testes unitários.
- Centralização da lógica de consulta: Toda a complexidade de queries fica encapsulada em um único local.
É importante diferenciar Repository de DAO (Data Access Object). Enquanto o DAO foca em operações CRUD genéricas e expõe a estrutura do banco, o Repository trabalha com conceitos do domínio, retornando entidades ou agregados completos. O Repository esconde a complexidade de como os dados são montados.
2. Estrutura Básica de um Repository em PHP
A base do padrão é uma interface que define os contratos:
<?php
namespace App\Domain\Repository;
use App\Domain\Entity\User;
use App\Domain\ValueObject\UserId;
interface UserRepositoryInterface
{
public function find(UserId $id): ?User;
public function findAll(): array;
public function save(User $user): void;
public function delete(UserId $id): void;
}
Implementação com PDO
<?php
namespace App\Infrastructure\Persistence;
use App\Domain\Entity\User;
use App\Domain\Repository\UserRepositoryInterface;
use App\Domain\ValueObject\UserId;
use PDO;
class PdoUserRepository implements UserRepositoryInterface
{
public function __construct(private PDO $pdo) {}
public function find(UserId $id): ?User
{
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id->value()]);
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$data) {
return null;
}
return User::fromArray($data);
}
public function findAll(): array
{
$stmt = $this->pdo->query('SELECT * FROM users');
$users = [];
while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
$users[] = User::fromArray($data);
}
return $users;
}
public function save(User $user): void
{
$stmt = $this->pdo->prepare(
'INSERT INTO users (id, name, email) VALUES (:id, :name, :email)
ON DUPLICATE KEY UPDATE name = :name2, email = :email2'
);
$stmt->execute([
'id' => $user->id()->value(),
'name' => $user->name(),
'email' => $user->email()->value(),
'name2' => $user->name(),
'email2' => $user->email()->value(),
]);
}
public function delete(UserId $id): void
{
$stmt = $this->pdo->prepare('DELETE FROM users WHERE id = :id');
$stmt->execute(['id' => $id->value()]);
}
}
Implementação com Eloquent
<?php
namespace App\Infrastructure\Persistence;
use App\Domain\Entity\User;
use App\Domain\Repository\UserRepositoryInterface;
use App\Domain\ValueObject\UserId;
use App\Models\User as EloquentUser;
class EloquentUserRepository implements UserRepositoryInterface
{
public function find(UserId $id): ?User
{
$eloquentUser = EloquentUser::find($id->value());
return $eloquentUser ? User::fromArray($eloquentUser->toArray()) : null;
}
public function findAll(): array
{
return EloquentUser::all()
->map(fn(EloquentUser $user) => User::fromArray($user->toArray()))
->toArray();
}
public function save(User $user): void
{
EloquentUser::updateOrCreate(
['id' => $user->id()->value()],
[
'name' => $user->name(),
'email' => $user->email()->value(),
]
);
}
public function delete(UserId $id): void
{
EloquentUser::destroy($id->value());
}
}
3. Repository com Tipagem Forte e Value Objects
Value Objects trazem segurança e expressividade ao código. Exemplos:
<?php
namespace App\Domain\ValueObject;
class UserId
{
public function __construct(private string $uuid)
{
if (!uuid_is_valid($uuid)) {
throw new \InvalidArgumentException('UUID inválido');
}
}
public function value(): string
{
return $this->uuid;
}
public function equals(UserId $other): bool
{
return $this->uuid === $other->uuid;
}
}
class Email
{
public function __construct(private string $email)
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Email inválido');
}
}
public function value(): string
{
return $this->email;
}
}
Métodos de busca especializados e uso de DTOs:
<?php
namespace App\Domain\Repository;
use App\Domain\DTO\UserSummaryDTO;
use App\Domain\ValueObject\Email;
use App\Domain\ValueObject\UserId;
interface UserRepositoryInterface
{
// Métodos existentes...
public function findByEmail(Email $email): ?User;
public function findActiveUsers(int $limit = 10): array;
public function findSummary(UserId $id): ?UserSummaryDTO;
}
4. Injeção de Dependência e Service Layer
A Service Layer utiliza o Repository através de sua interface:
<?php
namespace App\Application\Service;
use App\Domain\Entity\User;
use App\Domain\Repository\UserRepositoryInterface;
use App\Domain\ValueObject\Email;
use App\Domain\ValueObject\UserId;
class UserService
{
public function __construct(
private UserRepositoryInterface $userRepository
) {}
public function registerUser(string $name, string $email): User
{
$emailVo = new Email($email);
if ($this->userRepository->findByEmail($emailVo)) {
throw new \DomainException('Email já cadastrado');
}
$user = User::create($name, $emailVo);
$this->userRepository->save($user);
return $user;
}
public function getUserSummary(string $userId): array
{
$userIdVo = new UserId($userId);
$summary = $this->userRepository->findSummary($userIdVo);
if (!$summary) {
throw new \DomainException('Usuário não encontrado');
}
return $summary->toArray();
}
}
No Laravel, a injeção pode ser configurada no AppServiceProvider:
<?php
namespace App\Providers;
use App\Domain\Repository\UserRepositoryInterface;
use App\Infrastructure\Persistence\EloquentUserRepository;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
UserRepositoryInterface::class,
EloquentUserRepository::class
);
}
}
5. Testabilidade e Mocks
Testes unitários com mocks do Repository:
<?php
namespace Tests\Unit\Application\Service;
use App\Application\Service\UserService;
use App\Domain\Entity\User;
use App\Domain\Repository\UserRepositoryInterface;
use App\Domain\ValueObject\Email;
use PHPUnit\Framework\TestCase;
class UserServiceTest extends TestCase
{
public function testRegisterUserCreatesNewUser(): void
{
$repository = $this->createMock(UserRepositoryInterface::class);
$repository->expects($this->once())
->method('findByEmail')
->willReturn(null);
$repository->expects($this->once())
->method('save')
->with($this->isInstanceOf(User::class));
$service = new UserService($repository);
$user = $service->registerUser('João', 'joao@email.com');
$this->assertInstanceOf(User::class, $user);
}
}
Testes de integração com SQLite:
<?php
namespace Tests\Integration\Infrastructure\Persistence;
use App\Domain\Entity\User;
use App\Domain\ValueObject\Email;
use App\Domain\ValueObject\UserId;
use App\Infrastructure\Persistence\PdoUserRepository;
use PHPUnit\Framework\TestCase;
class PdoUserRepositoryTest extends TestCase
{
private PdoUserRepository $repository;
private \PDO $pdo;
protected function setUp(): void
{
$this->pdo = new \PDO('sqlite::memory:');
$this->pdo->exec('CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT, email TEXT)');
$this->repository = new PdoUserRepository($this->pdo);
}
public function testSaveAndFindUser(): void
{
$user = User::create('Maria', new Email('maria@email.com'));
$this->repository->save($user);
$found = $this->repository->find($user->id());
$this->assertNotNull($found);
$this->assertEquals('Maria', $found->name());
}
}
6. Repository com Cache e Performance
Um decorator para cache com Redis:
<?php
namespace App\Infrastructure\Cache;
use App\Domain\Entity\User;
use App\Domain\Repository\UserRepositoryInterface;
use App\Domain\ValueObject\UserId;
use Predis\Client as Redis;
class CachedUserRepository implements UserRepositoryInterface
{
private const TTL = 3600;
public function __construct(
private UserRepositoryInterface $repository,
private Redis $redis
) {}
public function find(UserId $id): ?User
{
$cacheKey = "user:{$id->value()}";
$cached = $this->redis->get($cacheKey);
if ($cached) {
return User::fromJson($cached);
}
$user = $this->repository->find($id);
if ($user) {
$this->redis->setex($cacheKey, self::TTL, $user->toJson());
}
return $user;
}
public function save(User $user): void
{
$this->repository->save($user);
$this->redis->del("user:{$user->id()->value()}");
}
// Outros métodos delegam para o repositório real...
}
7. Repository e Dados em Tempo Real
Integração com WebSockets ao salvar:
<?php
namespace App\Infrastructure\Persistence;
use App\Domain\Entity\User;
use App\Domain\Repository\UserRepositoryInterface;
use App\Infrastructure\WebSocket\WebSocketBroadcaster;
class WebSocketAwareUserRepository implements UserRepositoryInterface
{
public function __construct(
private UserRepositoryInterface $repository,
private WebSocketBroadcaster $broadcaster
) {}
public function save(User $user): void
{
$this->repository->save($user);
$this->broadcaster->broadcast('user.saved', $user->toArray());
}
// Demais métodos delegados...
}
8. Boas Práticas e Armadilhas Comuns
Evite o "God Repository": Não crie um repositório com dezenas de métodos. Separe por agregados ou contextos.
Repository vs. Query Objects: Para consultas complexas com muitos filtros, considere criar Query Objects separados:
<?php
namespace App\Domain\Repository\Query;
class ActiveUsersQuery
{
public function __construct(
public readonly ?int $minAge = null,
public readonly ?string $city = null,
public readonly string $orderBy = 'created_at',
public readonly int $limit = 20
) {}
}
Transações: Gerencie transações no nível do Service, não do Repository:
<?php
namespace App\Application\Service;
class OrderService
{
public function placeOrder(OrderData $data): void
{
$this->entityManager->beginTransaction();
try {
$order = Order::create($data);
$this->orderRepository->save($order);
foreach ($data->items as $item) {
$this->inventoryRepository->decrementStock($item->productId, $item->quantity);
}
$this->entityManager->commit();
} catch (\Exception $e) {
$this->entityManager->rollback();
throw $e;
}
}
}
O Repository Pattern, quando bem aplicado, traz desacoplamento, testabilidade e manutenibilidade para aplicações PHP de qualquer porte. A chave está em manter a interface focada no domínio e a implementação flexível para mudanças.
Referências
- PHP: The Right Way - Design Patterns — Guia oficial da comunidade PHP abordando o Repository Pattern e outros padrões de design.
- Laravel Documentation - Repository Pattern — Documentação oficial do Laravel com exemplos de implementação do Repository Pattern usando Eloquent.
- Doctrine ORM - Repository Pattern — Documentação do Doctrine ORM explicando como implementar Repositories com mapeamento objeto-relacional.
- Martin Fowler - Repository Pattern — Artigo clássico de Martin Fowler definindo o padrão Repository no contexto de arquitetura de software.
- PHP-DI - Dependency Injection Containers — Documentação do PHP-DI mostrando como configurar injeção de dependência para Repositories em containers modernos.
- PHPUnit - Testing with Mocks — Guia oficial do PHPUnit sobre criação de mocks para testar Repositories em isolamento.
- Redis PHP - Caching Strategies — Documentação do Redis para PHP com estratégias de cache aplicáveis a Repositories.