← Tilbage til patterns

Repository Pattern

Medierer mellem domain og data mapping lag ved at fungere som en in-memory collection af domain objekter.

StructuralMellem

Om Patterned

Repository Pattern blev introduceret af Eric Evans i hans bog 'Domain-Driven Design' fra 2003 og senere formaliseret af Martin Fowler i 'Patterns of Enterprise Application Architecture'. Patterned fungerer ved at indkapsle data access logic bag en objekt-orienteret interface, hvilket skaber en abstraktion mellem business logic og data persistence. Dette er fundamentalt i clean architecture og domain-driven design hvor du vil isolere domain logic fra infrastructure concerns som databases, APIs eller file systems. Repository Pattern gør det muligt at skifte data sources uden at påvirke business logic, forbedrer testbarhed ved at kunne mock repositories, og centraliserer query logic ét sted. I moderne PHP frameworks som Laravel og Symfony bruges Repository Pattern extensively sammen med ORM'er som Doctrine og Eloquent til at organisere database operationer på en vedligeholdelig måde.

Key Points

  • Abstraktion mellem domain logic og data persistence
  • Simulerer in-memory collection af objekter
  • Centraliserer data access logic
  • Fremmer Separation of Concerns
  • Gør det let at skifte data sources
  • Forbedrer testbarhed ved at kunne mock repositories
  • Tilbyder collection-like interface (add, remove, find)
  • Kan implementere Specification Pattern for komplekse queries
  • Ofte brugt sammen med Unit of Work Pattern
  • Fundamentalt i Domain-Driven Design
  • Repository kender til domain objekter, ikke database detaljer
  • Kan cache query results internt

Kode Eksempel

<?php

declare(strict_types=1);

namespace App\User;

use PDO;

/**
 * Domain Entity
 */
final class User
{
    public function __construct(
        private ?int $id,
        private string $email,
        private string $name,
        private bool $active = true,
        private ?\DateTimeImmutable $createdAt = null
    ) {
        $this->createdAt ??= new \DateTimeImmutable();
    }
    
    public function getId(): ?int
    {
        return $this->id;
    }
    
    public function setId(int $id): void
    {
        $this->id = $id;
    }
    
    public function getEmail(): string
    {
        return $this->email;
    }
    
    public function getName(): string
    {
        return $this->name;
    }
    
    public function setName(string $name): void
    {
        $this->name = $name;
    }
    
    public function isActive(): bool
    {
        return $this->active;
    }
    
    public function activate(): void
    {
        $this->active = true;
    }
    
    public function deactivate(): void
    {
        $this->active = false;
    }
    
    public function getCreatedAt(): \DateTimeImmutable
    {
        return $this->createdAt;
    }
}

/**
 * Repository Interface
 */
interface UserRepositoryInterface
{
    public function save(User $user): void;
    public function findById(int $id): ?User;
    public function findByEmail(string $email): ?User;
    public function findAll(): array;
    public function findActiveUsers(): array;
    public function delete(User $user): void;
    public function count(): int;
}

/**
 * Concrete Repository - Database Implementation
 */
final class DatabaseUserRepository implements UserRepositoryInterface
{
    public function __construct(
        private readonly PDO $pdo
    ) {}
    
    public function save(User $user): void
    {
        if ($user->getId() === null) {
            $this->insert($user);
        } else {
            $this->update($user);
        }
    }
    
    private function insert(User $user): void
    {
        $sql = "INSERT INTO users (email, name, active, created_at) 
                VALUES (:email, :name, :active, :created_at)";
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            'email' => $user->getEmail(),
            'name' => $user->getName(),
            'active' => $user->isActive() ? 1 : 0,
            'created_at' => $user->getCreatedAt()->format('Y-m-d H:i:s'),
        ]);
        
        $user->setId((int) $this->pdo->lastInsertId());
    }
    
    private function update(User $user): void
    {
        $sql = "UPDATE users 
                SET name = :name, active = :active 
                WHERE id = :id";
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            'id' => $user->getId(),
            'name' => $user->getName(),
            'active' => $user->isActive() ? 1 : 0,
        ]);
    }
    
    public function findById(int $id): ?User
    {
        $sql = "SELECT * FROM users WHERE id = :id";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(['id' => $id]);
        
        $row = $stmt->fetch();
        
        return $row ? $this->hydrate($row) : null;
    }
    
    public function findByEmail(string $email): ?User
    {
        $sql = "SELECT * FROM users WHERE email = :email";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(['email' => $email]);
        
        $row = $stmt->fetch();
        
        return $row ? $this->hydrate($row) : null;
    }
    
    public function findAll(): array
    {
        $sql = "SELECT * FROM users ORDER BY created_at DESC";
        $stmt = $this->pdo->query($sql);
        
        return array_map(
            fn($row) => $this->hydrate($row),
            $stmt->fetchAll()
        );
    }
    
    public function findActiveUsers(): array
    {
        $sql = "SELECT * FROM users WHERE active = 1 ORDER BY name";
        $stmt = $this->pdo->query($sql);
        
        return array_map(
            fn($row) => $this->hydrate($row),
            $stmt->fetchAll()
        );
    }
    
    public function delete(User $user): void
    {
        $sql = "DELETE FROM users WHERE id = :id";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(['id' => $user->getId()]);
    }
    
    public function count(): int
    {
        $sql = "SELECT COUNT(*) FROM users";
        return (int) $this->pdo->query($sql)->fetchColumn();
    }
    
    private function hydrate(array $row): User
    {
        return new User(
            id: (int) $row['id'],
            email: $row['email'],
            name: $row['name'],
            active: (bool) $row['active'],
            createdAt: new \DateTimeImmutable($row['created_at'])
        );
    }
}

/**
 * Alternative Implementation - In-Memory for Testing
 */
final class InMemoryUserRepository implements UserRepositoryInterface
{
    private array $users = [];
    private int $nextId = 1;
    
    public function save(User $user): void
    {
        if ($user->getId() === null) {
            $user->setId($this->nextId++);
        }
        
        $this->users[$user->getId()] = $user;
    }
    
    public function findById(int $id): ?User
    {
        return $this->users[$id] ?? null;
    }
    
    public function findByEmail(string $email): ?User
    {
        foreach ($this->users as $user) {
            if ($user->getEmail() === $email) {
                return $user;
            }
        }
        
        return null;
    }
    
    public function findAll(): array
    {
        return array_values($this->users);
    }
    
    public function findActiveUsers(): array
    {
        return array_filter(
            $this->users,
            fn(User $user) => $user->isActive()
        );
    }
    
    public function delete(User $user): void
    {
        unset($this->users[$user->getId()]);
    }
    
    public function count(): int
    {
        return count($this->users);
    }
}

// Brug af Repository Pattern
// Business logic kender kun til UserRepositoryInterface
final readonly class UserService
{
    public function __construct(
        private UserRepositoryInterface $repository
    ) {}
    
    public function registerUser(string $email, string $name): User
    {
        // Business logic: check if user exists
        if ($this->repository->findByEmail($email) !== null) {
            throw new \RuntimeException('User already exists');
        }
        
        $user = new User(null, $email, $name);
        $this->repository->save($user);
        
        return $user;
    }
    
    public function getActiveUsersCount(): int
    {
        return count($this->repository->findActiveUsers());
    }
}

// Kan bruge database repository i production
// $pdo = new PDO(...);
// $repository = new DatabaseUserRepository($pdo);

// Kan bruge in-memory repository i tests
$repository = new InMemoryUserRepository();
$service = new UserService($repository);

$user = $service->registerUser('test@example.com', 'Test User');
echo "Registered user: {$user->getName()} (ID: {$user->getId()})\n";

Fordele

  • +Separation of Concerns - isolerer data access fra business logic
  • +Testbarhed - let at mock repositories i unit tests
  • +Fleksibilitet - skift data source uden at ændre business logic
  • +Centraliseret query logic - DRY principle
  • +Domain-focused - arbejd med objekter i stedet for database rows
  • +Kan implementere caching transparent for consumers

Ulemper

  • Ekstra abstraction layer kan virke overkill for simple CRUD
  • Kan føre til code duplication mellem repositories
  • Learning curve for nye udviklere
  • Potential performance overhead ved abstraktion

Hvornår bruges det?

  • Database operations - abstraher database queries bag repository interface
  • API integrations - repository kan hente data fra eksterne APIs
  • Caching layer - repository kan cache frequently accessed data
  • Testing - brug in-memory repository i unit tests
  • Multi-tenancy - forskellige repository implementations per tenant
  • Data migration - skift mellem data sources transparent

Best Practices

  • Definer repository interface i domain layer
  • Implementer concrete repositories i infrastructure layer
  • Brug dependency injection til at inject repositories
  • Returnér domain objekter, ikke arrays eller DTOs
  • Hold repositories focused - én repository per aggregate root
  • Undgå at lække database detaljer i repository interface
  • Implementer in-memory repository til testing
  • Overvej Specification Pattern for komplekse queries

Quick Info

Kategori
Structural
Sværhedsgrad
Mellem
Relaterede Patterns
  • Unit of Work - tracker changes og koordinerer persistence
  • Specification Pattern - indkapsler query logic i objekter
  • Data Mapper - separerer domain objekter fra database schema