So verwenden Sie das Repository-Muster in einer Laravel-Anwendung

October 01, 2021
Autor:in:
Oluyemi Olususi
Mitwirkende:r
Die von Twilio-Mitwirkenden geäußerten Meinungen sind ihre eigenen
Prüfer:in:

Ein Repository übernimmt eine Art Vermittlerrolle zwischen der Domänen- und der Datenzuordnungsschicht und fungiert als zentrale Schnittstelle für den Zugriff auf Domänenobjekte.

Moderne PHP-Frameworks wie Laravel und Symfony interagieren mit Datenbanken über so genannte ORMs (Object-Relational Mapper; objektrelationale Abbildung). Symfony nutzt Doctrine als Standard-ORM und Laravel verwendet Eloquent.

Beide verfolgen unterschiedliche Ansätze bei der Interaktion mit ihren Datenbanken. Mit Eloquent werden für jede Datenbanktabelle Modelle generiert, die die Grundlage der Interaktion bilden. Doctrine hingegen nutzt das Repository-Muster, bei dem jede Entität über ein entsprechendes Repository verfügt, das über Helferfunktionen die Interaktion mit der Datenbank ermöglicht. Diese Funktionen werden nicht direkt von Laravel bereitgestellt, man kann das Repository-Muster aber in Laravel-Projekten einsetzen.

Ein wesentlicher Vorteil des Repository-Musters ist, dass es uns mit dem Abhängigkeits-Umkehr-Prinzip (Dependency Inversion Principle, DIP) die Abhängigkeit von Abstraktionen statt von konkreten Implementierungen ermöglicht. Dieses Prinzip macht den Code robuster gegen Veränderungen, zum Beispiel wenn erst später entschieden wird, eine Datenquelle zu verwenden, die nicht von Eloquent unterstützt wird.

Es hilft auch, den Code strukturiert zu halten und Dopplungen zu vermeiden, da sich die Datenbanklogik an einem zentralen Ort befindet. In kleinen Projekten kommt dieser Vorteil weniger zum Tragen, erst bei Großprojekten, die über viele Jahre gepflegt werden müssen, zahlt er sich aus.

In diesem Artikel erfahren Sie, wie das Repository-Muster in Laravel-Anwendungen implementiert wird. Dafür werden wir eine API entwerfen, die den Bestelleingang für ein Unternehmen verwaltet.

Voraussetzungen

Erste Schritte

Erstellen Sie ein neues Laravel-Projekt und wechseln Sie mit den folgenden Befehlen in das Verzeichnis.

laravel new order_api
cd order_api

Richten Sie die Datenbank ein

In diesem Tutorial nutzen wir MySQL als Datenbank. Aktualisieren Sie dazu wie unten dargestellt die Datenbankparameter in der .env-Datei.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=order_api
DB_USERNAME=<YOUR_DATABASE_USERNAME>
DB_PASSWORD=<YOUR_DATABASE_PASSWORD>

Erstellen Sie zum Abschluss mit Ihrem bevorzugten Datenbankmanagementsystem eine neue Datenbank mit dem Namen order_api.

Generieren Sie die ersten Daten für die Datenbank

Unsere Anwendung ist für die Verwaltung von Bestellungen gedacht, daher erstellen wir mit dem folgenden Befehl zunächst ein entsprechendes Modell.

php artisan make:model Order -a

Das Argument -a sagt Artisan, dass wir eine Migrationsdatei , einen Seeder, eine Factory und einen Controller für das Modell Order (Bestellung) erstellen möchten.

Mit dem obigen Befehl werden fünf neue Dateien erstellt:

  • Ein Controller in app/Http/Controllers/OrderController.php
  • Eine Datenbank-Factory in database/factories/orderFactory.php
  • Eine Migrationsdatei in database/migrations/YYYY_MM_DD_HHMMSS_create_orders_table.php
  • Ein Modell in app/Models/Order.php
  • Eine Seeder-Datei in database/seeders/OrderSeeder.php 

Aktualisieren Sie in database/migrations/YYYY_MM_DD_HHMMSS_create_orders_table.php die Funktion up wie folgt.

public function up()
{
    Schema::create('orders', function (Blueprint $table) {
        $table->id();
        $table->text('details');
        $table->string('client');
        $table->boolean('is_fulfilled')->default(false);
        $table->timestamps();
    });
 }

Wie in der Migrationsdatei angegeben, enthält die Tabelle order die folgenden Spalten:

  1. Eine ID. Sie ist der Primärschlüssel der Tabelle.
  2. Die Details der Bestellung.
  3. Name des bzw. der Kundin, die die Bestellung aufgegeben hat.
  4. Ob die Bestellung abgeschlossen ist oder nicht.
  5. Wann die Bestellung erstellt und aktualisiert wurde (created_at und updated_at) basierend auf der Funktion timestamps.

Als Nächstes aktualisieren wir OrderFactory, um einen Dummy zum Füllen der Datenbank zu generieren. Aktualisieren Sie in database/factories/OrderFactory.php die Funktion definition wie folgt.

public function definition() 
{
    return [
        'details'       => $this->faker->sentences(4, true),
        'client'         => $this->faker->name(),
        'is_fulfilled' => $this->faker->boolean(),
    ];
}

Öffnen Sie anschließend database/seeders/OrderSeeder.php und aktualisieren Sie die Funktion run wie folgt.

public function run() 
{
    Order::factory()->times(50)->create();
}

Diese Funktion erstellt mit OrderFactory 50 Bestellungen in der Datenbank.

Don't forget to add this import:

use AppModelsOrder;

Fügen Sie in src/database/seeders/DatabaseSeeder.php Folgendes zur Funktion run hinzu.

$this->call(
    [
        OrderSeeder::class
    ]
);

Damit wird QuoteSeeder ausgeführt, wenn der db:seed-Befehl von Artisan ausgeführt wird.

Führen Sie zum Abschluss Ihre Migrationen aus und füllen Sie die Datenbank mit dem folgenden Befehl.

php artisan migrate --seed

Wenn Sie die Tabelle orders öffnen, sehen Sie die neuen Einträge für Bestellungen.

Liste der Bestellungen

Erstellen Sie das Repository

Bevor wir ein Repository für das Modell Order erstellen, brauchen wir noch eine Schnittstelle mit der Deklaration aller Methoden. Unser Controller (und alle künftigen order-Komponenten) soll nicht direkt von der Repository-Klasse, sondern von der Schnittstelle abhängen.

Dadurch wird der Code flexibler, weil sich künftige Änderungen nicht auf den Controller auswirken. Wenn Sie sich irgendwann entscheiden, die Bestellverwaltung an die Anwendung eines Drittanbieters auszulagern, können Sie ein neues Modul passend zur Signatur von OrderRepositoryInterface entwerfen und die Bindungsdeklarationen austauschen. Der Controller funktioniert trotzdem wie erwartet, ohne dass eine einzige Codezeile im Controller geändert werden muss.

Erstellen Sie im Verzeichnis App einen neuen Ordner mit dem Namen Interfaces. Erstellen Sie anschließend in Interfaces eine neue Datei namens OrderRepositoryInterface.php und fügen Sie darin den folgenden Code ein.

<?php

namespace App\Interfaces;

interface OrderRepositoryInterface 
{
    public function getAllOrders();
    public function getOrderById($orderId);
    public function deleteOrder($orderId);
    public function createOrder(array $orderDetails);
    public function updateOrder($orderId, array $newDetails);
    public function getFulfilledOrders();
}

Erstellen Sie dann im Verzeichnis App einen neuen Ordner namens Repositories. Erstellen Sie in diesem Ordner eine neue Datei namens OrderRepository.php und fügen Sie darin den folgenden Code ein.

<?php

namespace App\Repositories;

use App\Interfaces\OrderRepositoryInterface;
use App\Models\Order;

class OrderRepository implements OrderRepositoryInterface 
{
    public function getAllOrders() 
    {
        return Order::all();
    }

    public function getOrderById($orderId) 
    {
        return Order::findOrFail($orderId);
    }

    public function deleteOrder($orderId) 
    {
        Order::destroy($orderId);
    }

    public function createOrder(array $orderDetails) 
    {
        return Order::create($orderDetails);
    }

    public function updateOrder($orderId, array $newDetails) 
    {
        return Order::whereId($orderId)->update($newDetails);
    }

    public function getFulfilledOrders() 
    {
        return Order::where('is_fulfilled', true);
    }
}

Abgesehen von der Flexibilität, die die Schnittstelle bietet, hat eine derartige Verkapselung der Abfragen den zusätzlichen Vorteil, dass Sie Abfragen innerhalb der Anwendung nicht duplizieren müssen.

Wenn Sie künftig beschließen, mit der Funktion getAllOrders() nur Bestellungen abzurufen, die nicht ausgeführt wurden, müssen Sie nur an einer Stelle etwas ändern, statt alle Deklarationen von Order::all() herauszusuchen und dabei das Risiko einzugehen, dass Sie einige übersehen.

Erstellen Sie die Controller

Nachdem das Repository fertig ist, können Sie jetzt den Code für den Controller hinzufügen. Öffnen Sie app/Http/Controllers/OrderController.php und aktualisieren Sie den Code wie folgt.

<?php

namespace App\Http\Controllers;

use App\Interfaces\OrderRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class OrderController extends Controller 
{
    private OrderRepositoryInterface $orderRepository;

    public function __construct(OrderRepositoryInterface $orderRepository) 
    {
        $this->orderRepository = $orderRepository;
    }

    public function index(): JsonResponse 
    {
        return response()->json([
            'data' => $this->orderRepository->getAllOrders()
        ]);
    }

    public function store(Request $request): JsonResponse 
    {
        $orderDetails = $request->only([
            'client',
            'details'
        ]);

        return response()->json(
            [
                'data' => $this->orderRepository->createOrder($orderDetails)
            ],
            Response::HTTP_CREATED
        );
    }

    public function show(Request $request): JsonResponse 
    {
        $orderId = $request->route('id');

        return response()->json([
            'data' => $this->orderRepository->getOrderById($orderId)
        ]);
    }

    public function update(Request $request): JsonResponse 
    {
        $orderId = $request->route('id');
        $orderDetails = $request->only([
            'client',
            'details'
        ]);

        return response()->json([
            'data' => $this->orderRepository->updateOrder($orderId, $orderDetails)
        ]);
    }

    public function destroy(Request $request): JsonResponse 
    {
        $orderId = $request->route('id');
        $this->orderRepository->deleteOrder($orderId);

        return response()->json(null, Response::HTTP_NO_CONTENT);
    }
}

Über den Konstruktor fügt der Code die Instanz OrderRepositoryInterface ein, wobei die entsprechenden Objektmethoden aus der jeweiligen Controller-Methode zur Anwendung kommen.

Im Rahmen der Methode index() ruft er die im orderRepository definierte Methode getAllOrders() auf, um die Liste der Bestellungen abzurufen, und gibt eine Antwort im JSON-Format zurück.

Anschließend ruft die Methode store() die Methode createOrder() aus dem orderRepository auf, um eine neue Bestellung zu erstellen. Damit werden die Details der Bestellung als Array erstellt und anschließend wird eine erfolgreiche Antwort zurückgegeben.

Im Controller wird innerhalb der Methode show() die eindeutige Id der Bestellung aus der Route abgerufen und als Parameter an die Methode getOrderById() übergeben. Damit werden die Details der Bestellung mit der passenden ID aus der Datenbank abgerufen und eine Antwort im JSON-Format zurückgegeben.

Um anschließend die Details einer bereits erstellten Bestellung zu aktualisieren, wird aus dem Repository die Methode updateOrder() aufgerufen. Dazu sind zwei Parameter nötig: die eindeutige ID der Bestellung und die zu aktualisierenden Details.

Abschließend ruft die Methode destroy() die eindeutige ID einer bestimmten Bestellung aus der Route ab und löscht sie mit der Methode deleteOrder() aus dem Repository.

Fügen Sie Routen hinzu

Um jede der im Controller definierten Methoden einer spezifischen Route zuzuordnen, müssen Sie den folgenden Code zu routes/api.php hinzufügen.

Route::get('orders', [OrderController::class, 'index']);
Route::get('orders/{id}', [OrderController::class, 'show']);
Route::post('orders', [OrderController::class, 'store']);
Route::put('orders/{id}', [OrderController::class, 'update']);
Route::delete('orders/{id}', [OrderController::class, 'delete']);

Remember to include the import statement for the OrderController.

use App\Http\Controllers\OrderController;

Binden Sie Schnittstelle und Implementierung

Der letzte Schritt besteht darin, das OrderRepository an das OrderRepositoryInterface im Service Container von Laravel zu binden. Dafür nutzen wir einen Serviceanbieter. Erstellen Sie einen Anbieter über den folgenden Befehl.

php artisan make:provider RepositoryServiceProvider

Öffnen Sie app/Providers/RepositoryServiceProvider.php und aktualisieren Sie die Funktion register wie folgt.

public function register() 
{
    $this->app->bind(OrderRepositoryInterface::class, OrderRepository::class);
 }

Remember to include the import statement for OrderRepository and OrderRepositoryInterface.

use App\Interfaces\OrderRepositoryInterface;
use App\Repositories\OrderRepository;

Fügen Sie abschließend den neuen Serviceanbieter zum Array providers in config/app.php hinzu.

'providers' => [
    // ...other declared providers
    App\Providers\RepositoryServiceProvider::class,
];

Testen Sie die Anwendung

Führen Sie die Anwendung mithilfe des folgenden Befehls aus.

php artisan serve

Standardmäßig ist die zugehörige Anwendung unter http://127.0.0.1:8000/ verfügbar. Mit Postman oder cURL können Sie Abfragen an die neu erstellte API stellen.

Führen Sie den folgenden Befehl aus, um den Endpunkt /api/orders mit cURL zu testen:

curl --silent http://localhost:8000/api/orders | jq

The response was formatted to JSON using jq.

Die JSON-Ausgabe in Ihrem Terminal sollte in etwa so aussehen wie das Beispiel unten, das zur besseren Lesbarkeit gekürzt wurde.

{
  "data": [
    {
      "id": 1,
      "details": "Sit ullam cupiditate dolorem in. Magnam suscipit eaque occaecati facilis amet illum. Dolor perspiciatis velit laboriosam. Enim fugiat excepturi qui natus incidunt dolorem debitis ut.",
      "client": "Cydney Conn V",
      "is_fulfilled": 0,
      "created_at": "2021-09-09T09:18:28.000000Z",
      "updated_at": "2021-09-09T09:18:28.000000Z"
    },
    {
      "id": 2,
      "details": "Eum iste eum molestiae est. Voluptatibus veritatis earum commodi. Quod et laboriosam ratione dolor adipisci. Nam et debitis nobis ea sit.",
      "client": "Willow Herzog",
      "is_fulfilled": 1,
      "created_at": "2021-09-09T09:18:28.000000Z",
      "updated_at": "2021-09-09T09:18:28.000000Z"
    },
    {
      "id": 3,
      "details": "At maxime architecto repellat quidem id. Saepe provident quo eos officiis et tenetur. Et expedita maxime atque. Et consequuntur sequi aperiam possimus odio est ab.",
      "client": "Mr. Peyton Nolan DVM",
      "is_fulfilled": 1,
      "created_at": "2021-09-09T09:18:28.000000Z",
      "updated_at": "2021-09-09T09:18:28.000000Z"
    }
  ]
}

Und so verwenden Sie das Repository-Muster in einer Laravel-Anwendung

In diesem Artikel haben Sie das Repository-Muster und dessen Verwendung in einer Laravel-Anwendung kennengelernt. Sie haben auch von einigen Vorteilen für große Projekte erfahren – einer davon ist lose gekoppelter Code, bei dem Sie nach Abstraktionen statt nach konkreten Implementierungen programmieren.

Noch ein abschließender Hinweis: Für kleine Projekte ist dieser Ansatz ziemlich arbeitsintensiv und basiert auf Bausteinen, deren Vorteile sich nicht unbedingt auf den ersten Blick erschließen. Deshalb sollten Sie sich gut überlegen, ob der Umfang Ihres Projekts diese Vorgehensweise rechtfertigt.

Die gesamte Codebasis dieses Tutorials finden Sie auf GitHub. Informieren Sie sich dort gern weiter. Viel Spaß beim Programmieren!

Oluyemi ist Technikfan und kommt eigentlich aus dem Bereich der Telekommunikation. Da er so viel Spaß daran hat, Benutzenden bei ihren alltäglichen Problemen zu helfen, wagte er schließlich den Sprung in die Programmierung. Sein Hauptschwerpunkt liegt seitdem in der Entwicklung von Software für das Internet und mobile Endgeräte.

Oluyemi ist Full-Stack-Entwickler und gibt sein Wissen leidenschaftlich gern an andere weiter. Im Internet finden sich zahlreiche von ihm verfasste technische Artikel und Beiträge in verschiedenen Blogs. Als bekennender Technik-Freak zählt er auch das Austesten neuer Programmiersprachen und Frameworks zu seinen Hobbys.