Das Laravel Repository Pattern ist ein klassisches Entwurfsmuster, das die Logik des Datenzugriffs aus deinen Controllern und Services herauslöst und in eine eigene Schicht verlagert. Die Idee ist einfach: Dein Code soll nicht wissen, woher die Daten kommen (Eloquent, eine rohe Query, eine externe API); er fragt einfach eine UserRepository-Schnittstelle nach find() oder all() und erhält ein Ergebnis zurück. Das klingt sauber, ist aber eines der meistdiskutierten Themen in der Laravel-Community — denn Eloquent ist bereits eine Abstraktionsschicht, und eine zweite darüberzustapeln bringt in den meisten Projekten mehr Last als Nutzen. In diesem Artikel kläre ich, wie man das Muster richtig aufsetzt, was seine echten Vorteile sind, welche versteckten Kosten es hat und wann es ins Übertriebene kippt.
Was löst das Repository Pattern eigentlich?
Das Muster stammt aus dem Domain-Driven Design. Sein Ziel ist es, die Geschäftslogik (die Domäne) von den Details der Persistenz zu isolieren. Theoretisch verhält sich ein Repository wie eine Sammlung von Objekten im Speicher: Du fragst es nach Objekten, es gibt dir Objekte, und ob dahinter SQL, eine Datei oder eine REST-API steckt, geht dich nichts an. In der Praxis läuft das in Laravel auf drei Hauptziele hinaus:
- Abstraktion: Die Geschäftslogik hängt von einer Schnittstelle ab, nicht von einem konkreten ORM.
- Testbarkeit: Du kannst die Schnittstelle durch einen Mock ersetzen und Tests schreiben, ohne die Datenbank zu berühren.
- Wiederverwendung: Komplexe Queries leben an einem Ort, statt über Controller verstreut zu sein.
Die entscheidende Frage hier lautet: Bietet Eloquent als Active-Record-Implementierung nicht bereits einen Teil dieser Abstraktion? Doch — und genau diese Überschneidung ist der meistkritisierte Aspekt des Repository Pattern.
Die korrekte Einrichtung in Laravel
Wenn du das Muster anwendest, mach es richtig. Die Grundstruktur besteht aus einer Schnittstelle und einer konkreten Klasse, die sie implementiert. Zuerst die Schnittstelle:
namespace App\Repositories\Contracts;
interface UserRepositoryInterface
{
public function all();
public function find(int $id);
public function create(array $data);
}
Dann die auf Eloquent basierende konkrete Implementierung:
namespace App\Repositories\Eloquent;
use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;
class EloquentUserRepository implements UserRepositoryInterface
{
public function __construct(private User $model) {}
public function all()
{
return $this->model->all();
}
public function find(int $id)
{
return $this->model->findOrFail($id);
}
public function create(array $data)
{
return $this->model->create($data);
}
}
Der letzte Schritt ist, die Schnittstelle an die konkrete Klasse zu binden. Das machst du in einem Service Provider:
// in AppServiceProvider oder einem eigenen RepositoryServiceProvider
public function register(): void
{
$this->app->bind(
\App\Repositories\Contracts\UserRepositoryInterface::class,
\App\Repositories\Eloquent\EloquentUserRepository::class
);
}
Jetzt fragt der Controller nach der Schnittstelle und der Service-Container von Laravel injiziert die richtige Implementierung:
public function show(UserRepositoryInterface $users, int $id)
{
return view('users.show', [
'user' => $users->find($id),
]);
}
Beachte: Da die Bindung auf die Schnittstelle zielt, änderst du an dem Tag, an dem du Eloquent durch eine andere Implementierung ersetzen willst, nur die bind-Zeile. Auf dem Papier ist das das Versprechen des Musters.
Die echten Vorteile
Das Muster zahlt sich im richtigen Kontext wirklich aus. Seine konkretesten Vorteile sind:
- Komplexe Queries zentralisieren. Wenn dieselbe mehrstufige Reporting-Query in fünf Controllern dupliziert ist, verbessert das Auslagern in eine aussagekräftige Methode wie
topSpendingCustomers()in einemReportRepositorydie Lesbarkeit erheblich. - Mehrere Datenquellen. Wenn Daten teils aus MySQL und teils aus einer externen API kommen, ist ein Repository ein natürlicher Ort, um beide hinter einer einzigen Schnittstelle zusammenzuführen.
- Geschäftslogik vom ORM abschirmen. Ist deine Domänenschicht groß und langlebig, erleichtert es eine künftige ORM-Migration theoretisch, sie nicht eng an Eloquents Funktionen zu koppeln.
Beachte, dass diese Punkte ein Merkmal teilen: Das Projekt hat eine bestimmte Komplexitätsschwelle überschritten. In kleinen und mittleren Projekten treten diese Bedingungen selten ein.
Versteckte Kosten und Kritik
Die Community kritisiert das Muster aus guten Gründen häufig, und sie zu ignorieren wäre ein Fehler:
- Eloquent ist bereits eine Abstraktion.
User::find($id)ist selbst eine Abstraktion; es kann den Datenbanktreiber wechseln und erzeugt die Query, ohne dass du sie siehst. Noch eine darüberzulegen heißt oft, dasselbe doppelt zu tun. - Du verlierst die Stärke von Eloquent. Wenn dein Repository eine Collection zurückgibt, kann der aufrufende Code die flüssige Kette wie
where(),with()oderpaginate()nicht mehr nutzen. Um sie zu unterstützen, fügst du dem Repository Dutzende Methoden hinzu und erfindest am Ende Eloquent neu. - Das Szenario „Treiber wechseln" tritt fast nie ein. Die Hauptbegründung des Musters ist die Annahme „was, wenn wir morgen Eloquent verlassen". In der Praxis bleibt die überwältigende Mehrheit der Projekte ihr ganzes Leben lang bei Eloquent; du entwickelst für ein Problem, das es nicht gibt.
- Mehr Dateien, mehr Overhead. Eine Schnittstelle, eine konkrete Klasse und eine Bindungszeile pro Modell — ein Labyrinth, das ein neues Teammitglied für wenig Gewinn durchqueren muss.
Ist Testbarkeit wirklich eine Begründung?
Der stärkste Trumpf der Repository-Befürworter ist das Testen: Du mockst die Schnittstelle und testest, ohne die Datenbank zu berühren. Das stimmt, aber die Alternative in Laravel ist kostenlos. Eine schnelle In-Memory-SQLite-Datenbank mit dem RefreshDatabase-Trait ist für die meisten Tests schnell genug und übt deine echten Queries — eine gemockte Schnittstelle bleibt grün, selbst wenn deine Query-Logik kaputt ist.
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_benutzer_wird_erstellt(): void
{
$user = User::factory()->create(['email' => 'a@b.com']);
$this->assertDatabaseHas('users', ['email' => 'a@b.com']);
}
}
So bricht die Behauptung „man braucht ein Repository zum Testen" in den meisten Fällen an Laravels eigenen Werkzeugen zusammen. Testbarkeit ist das Ergebnis gut strukturierten Codes, nicht des Repositorys im Besonderen.
Eine praktische Alternative: eine dünne Service-Schicht oder Action-Klassen
In den meisten Fällen willst du eigentlich „Controller schlank halten" und „Geschäftslogik an einem Ort bündeln" — und das erfordert keine vollständige Repository-Abstraktion. Zwei leichtgewichtige Ansätze genügen. Service-Klassen gruppieren zusammengehörige Geschäftslogik und nutzen darin bequem Eloquent. Action-Klassen sind kleine Klassen mit einer einzigen Verantwortung und einer handle()-Methode:
class CreateUser
{
public function handle(array $data): User
{
$user = User::create($data);
$user->sendWelcomeNotification();
return $user;
}
}
Dieser Ansatz ordnet den Code, bewahrt die volle Stärke von Eloquent und vermeidet die überflüssige Schnittstellenschicht. Es ist der Weg, den viele erfahrene Laravel-Entwickler einem vollständigen Repository vorziehen.
Häufige Fragen
Sollte ich das Repository Pattern in jedem Laravel-Projekt verwenden?
Nein. In der überwiegenden Mehrheit kleiner und mittlerer Projekte ist es eine überflüssige Abstraktion; Eloquent direkt zu nutzen, bei Bedarf mit einer dünnen Service- oder Action-Schicht, ist einfacher und leichter zu warten. Ziehe das Muster nur in Betracht, wenn du wirklich mehrere Datenquellen, schwere und wiederholte Queries oder eine große ORM-unabhängige Domänenschicht hast.
Beeinflusst das Repository Pattern die Performance?
Es gibt keinen direkt messbaren Einfluss auf die Performance; die Kosten eines zusätzlichen Klassenaufrufs sind vernachlässigbar. Das eigentliche Risiko ist indirekt: Ein naives Repository, das Collections zurückgibt, kann das N+1-Query-Problem verursachen, wenn es den aufrufenden Code daran hindert, mit with() Eager Loading zu betreiben. Das ist die Folge einer schlechten Implementierung, nicht des Musters selbst.
Ersetzt das Repository Pattern Eloquent?
Nein, es setzt darauf auf, statt es zu ersetzen. Innerhalb des Repositorys verwendest du in der Regel weiterhin Eloquent; das Muster verbirgt diese Nutzung lediglich hinter einer Schnittstelle. Eloquent ganz aufzugeben ist keine Voraussetzung, um das Muster zu nutzen.
Braucht dein Laravel-Projekt wirklich das Repository Pattern, oder reicht eine einfachere Architektur? Wir können uns gemeinsam die Größe deines Projekts ansehen und die richtige Abstraktionsebene bestimmen — nimm Kontakt mit mir auf.