Repository Pattern Implementierung
Repository Pattern implementieren
Das Repository Pattern abstrahiert den Datenzugriff und entkoppelt die Geschäftslogik von der Persistenzschicht. Lernen Sie die Implementierung und Best Practices.
Was ist das Repository Pattern?
┌─────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ │
│ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Service │ ──────► │ Repository Interface│ │
│ └─────────────┘ └──────────┬──────────┘ │
│ │ │
├──────────────────────────────────────┼──────────────────────┤
│ INFRASTRUCTURE LAYER │ │
│ │ │
│ ┌────────────────────────────┴───────────────┐ │
│ │ │ │
│ ┌─────┴─────┐ ┌────────────┐ ┌──────────────┐ │ │
│ │ MySQL │ │ PostgreSQL │ │ InMemory │ │ │
│ │ Repository│ │ Repository │ │ Repository │ │ │
│ └───────────┘ └────────────┘ └──────────────┘ │ │
│ │ │
└──────────────────────────────────────────────────────┘ │
│
Grundlegendes Interface
// Generic Repository Interface
interface RepositoryInterface {
public function find(int $id): ?object;
public function findAll(): array;
public function save(object $entity): void;
public function delete(object $entity): void;
}
// Spezifisches Repository Interface
interface UserRepositoryInterface {
public function find(int $id): ?User;
public function findAll(): array;
public function findByEmail(string $email): ?User;
public function findActiveUsers(): array;
public function save(User $user): void;
public function delete(User $user): void;
}
Konkrete Implementierung
class MySQLUserRepository implements UserRepositoryInterface {
public function __construct(
private PDO $pdo
) {}
public function find(int $id): ?User {
$stmt = $this->pdo->prepare(
'SELECT * FROM users WHERE id = :id'
);
$stmt->execute(['id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? $this->hydrate($row) : null;
}
public function findAll(): array {
$stmt = $this->pdo->query('SELECT * FROM users');
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
return array_map([$this, 'hydrate'], $rows);
}
public function findByEmail(string $email): ?User {
$stmt = $this->pdo->prepare(
'SELECT * FROM users WHERE email = :email'
);
$stmt->execute(['email' => $email]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? $this->hydrate($row) : null;
}
public function findActiveUsers(): array {
$stmt = $this->pdo->query(
'SELECT * FROM users WHERE status = "active"'
);
return array_map([$this, 'hydrate'], $stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function save(User $user): void {
if ($user->getId() === null) {
$this->insert($user);
} else {
$this->update($user);
}
}
private function insert(User $user): void {
$stmt = $this->pdo->prepare(
'INSERT INTO users (name, email, status) VALUES (:name, :email, :status)'
);
$stmt->execute([
'name' => $user->getName(),
'email' => $user->getEmail(),
'status' => $user->getStatus()
]);
$user->setId((int) $this->pdo->lastInsertId());
}
private function update(User $user): void {
$stmt = $this->pdo->prepare(
'UPDATE users SET name = :name, email = :email, status = :status WHERE id = :id'
);
$stmt->execute([
'id' => $user->getId(),
'name' => $user->getName(),
'email' => $user->getEmail(),
'status' => $user->getStatus()
]);
}
public function delete(User $user): void {
$stmt = $this->pdo->prepare('DELETE FROM users WHERE id = :id');
$stmt->execute(['id' => $user->getId()]);
}
private function hydrate(array $row): User {
return new User(
id: (int) $row['id'],
name: $row['name'],
email: $row['email'],
status: $row['status']
);
}
}
In-Memory Repository für Tests
class InMemoryUserRepository implements UserRepositoryInterface {
private array $users = [];
private int $nextId = 1;
public function find(int $id): ?User {
return $this->users[$id] ?? null;
}
public function findAll(): array {
return array_values($this->users);
}
public function findByEmail(string $email): ?User {
foreach ($this->users as $user) {
if ($user->getEmail() === $email) {
return $user;
}
}
return null;
}
public function findActiveUsers(): array {
return array_filter(
$this->users,
fn(User $user) => $user->getStatus() === 'active'
);
}
public function save(User $user): void {
if ($user->getId() === null) {
$user->setId($this->nextId++);
}
$this->users[$user->getId()] = $user;
}
public function delete(User $user): void {
unset($this->users[$user->getId()]);
}
// Hilfsmethoden für Tests
public function clear(): void {
$this->users = [];
$this->nextId = 1;
}
public function count(): int {
return count($this->users);
}
}
Verwendung im Service
class UserService {
public function __construct(
private UserRepositoryInterface $userRepository,
private MailerInterface $mailer
) {}
public function register(string $name, string $email, string $password): User {
// Prüfen ob Email bereits existiert
if ($this->userRepository->findByEmail($email)) {
throw new UserAlreadyExistsException($email);
}
// User erstellen
$user = new User(
id: null,
name: $name,
email: $email,
status: 'pending'
);
$user->setPassword(password_hash($password, PASSWORD_DEFAULT));
// Speichern
$this->userRepository->save($user);
// Willkommens-Email senden
$this->mailer->sendWelcomeEmail($user);
return $user;
}
public function activate(int $userId): void {
$user = $this->userRepository->find($userId);
if (!$user) {
throw new UserNotFoundException($userId);
}
$user->setStatus('active');
$this->userRepository->save($user);
}
public function getActiveUsers(): array {
return $this->userRepository->findActiveUsers();
}
}
Criteria/Specification Pattern
// Für komplexe Abfragen: Specification Pattern
interface Specification {
public function toSql(): string;
public function getParameters(): array;
}
class EmailSpecification implements Specification {
public function __construct(private string $email) {}
public function toSql(): string {
return 'email = :email';
}
public function getParameters(): array {
return ['email' => $this->email];
}
}
class StatusSpecification implements Specification {
public function __construct(private string $status) {}
public function toSql(): string {
return 'status = :status';
}
public function getParameters(): array {
return ['status' => $this->status];
}
}
class AndSpecification implements Specification {
private array $specifications;
public function __construct(Specification ...$specifications) {
$this->specifications = $specifications;
}
public function toSql(): string {
$parts = array_map(
fn($spec) => $spec->toSql(),
$this->specifications
);
return '(' . implode(' AND ', $parts) . ')';
}
public function getParameters(): array {
return array_merge(
...array_map(
fn($spec) => $spec->getParameters(),
$this->specifications
)
);
}
}
// Erweitertes Repository Interface
interface UserRepositoryInterface {
// ... andere Methoden ...
public function findBySpecification(Specification $spec): array;
}
// Verwendung
$activeAdmins = $userRepository->findBySpecification(
new AndSpecification(
new StatusSpecification('active'),
new RoleSpecification('admin')
)
);
Mit Doctrine ORM
use Doctrine\ORM\EntityRepository;
class DoctrineUserRepository extends EntityRepository implements UserRepositoryInterface {
public function findByEmail(string $email): ?User {
return $this->findOneBy(['email' => $email]);
}
public function findActiveUsers(): array {
return $this->findBy(['status' => 'active']);
}
public function save(User $user): void {
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
}
public function delete(User $user): void {
$this->getEntityManager()->remove($user);
$this->getEntityManager()->flush();
}
// Komplexe Abfrage mit QueryBuilder
public function findRecentlyActive(int $days = 30): array {
$date = new DateTime("-{$days} days");
return $this->createQueryBuilder('u')
->where('u.lastLoginAt >= :date')
->andWhere('u.status = :status')
->setParameter('date', $date)
->setParameter('status', 'active')
->orderBy('u.lastLoginAt', 'DESC')
->getQuery()
->getResult();
}
}
Unit Tests
class UserServiceTest extends TestCase {
private InMemoryUserRepository $userRepository;
private FakeMailer $mailer;
private UserService $service;
protected function setUp(): void {
$this->userRepository = new InMemoryUserRepository();
$this->mailer = new FakeMailer();
$this->service = new UserService(
$this->userRepository,
$this->mailer
);
}
public function testRegisterCreatesUser(): void {
$user = $this->service->register('Max', 'max@test.de', 'secret');
$this->assertNotNull($user->getId());
$this->assertEquals('Max', $user->getName());
$this->assertEquals('pending', $user->getStatus());
}
public function testRegisterThrowsExceptionForDuplicateEmail(): void {
// Existing user
$existing = new User(null, 'Max', 'max@test.de', 'active');
$this->userRepository->save($existing);
$this->expectException(UserAlreadyExistsException::class);
$this->service->register('Another', 'max@test.de', 'secret');
}
public function testActivateChangesStatus(): void {
$user = new User(null, 'Max', 'max@test.de', 'pending');
$this->userRepository->save($user);
$this->service->activate($user->getId());
$updated = $this->userRepository->find($user->getId());
$this->assertEquals('active', $updated->getStatus());
}
}
💡 Vorteile des Repository Patterns:
1. Testbarkeit durch austauschbare Implementierungen
2. Entkopplung von Datenbank-Details
3. Zentrale Stelle für Datenzugriffs-Logik
4. Einfacher Wechsel des Speichersystems
5. Klare Schnittstellen für Datenzugriff
2. Entkopplung von Datenbank-Details
3. Zentrale Stelle für Datenzugriffs-Logik
4. Einfacher Wechsel des Speichersystems
5. Klare Schnittstellen für Datenzugriff