Laravelアプリケーションでリポジトリパターンを使うには

October 01, 2021
執筆者
Oluyemi Olususi
寄稿者
Twilio の寄稿者によって表明された意見は彼ら自身のものです
レビュー担当者

リポジトリは、ドメインとデータマッピングレイヤー間の抽象化レイヤーとして定義できます。これにより、コレクション的なインターフェイスを通じてドメインオブジェクトにアクセスして両者の仲介手段を提供します。

LaravelSymfonyなど、最新のPHPフレームワークは、オブジェクトリレーショナルマッパー(ORM)を介してデータベースを操作します。SymfonyのデフォルトORMはDoctrineで、LaravelはEloquentを使用します。

ともに、データベース操作のために異なるアプローチがとられます。Eloquentの場合、データベーステーブルごとにモデルが生成され、操作の基盤が形成されます。一方、Doctrineはリポジトリパターンを使用します。各エンティティには対応するリポジトリがあり、データベースを操作するヘルパー関数が格納されています。Laravelには標準でこの機能は提供されませんが、Laravelプロジェクトでリポジトリパターンを使用することが可能です。

リポジトリパターンを使う主なメリットは、依存性逆転の原則(具体化でなく抽象化コード)を使用できる点です。これにより、例えば後でEloquentがサポートしないデータソースに変更を決めた場合などに、変更の影響を受けにくいコードを作成できます。

データベース関連のロジックが一つの場所に維持され、コードの構成を維持して重複を避けることにもつながります。このようなメリットは小規模のプロジェクトではすぐに実感できませんが、長年かけて維持が必要な大規模プロジェクトでより顕著に感じることができます。

この記事では、Laravelアプリケーションにリポジトリパターンを実装する方法を紹介します。そのために、会社のクライアントから受けた注文を管理するAPIを作成します。

必要条件

はじめましょう

新規Laravelプロジェクトを作成し、次のようにcdコマンドでディレクトリを変更します。

laravel new order_api
cd order_api

データベースのセットアップ

このチュートリアルでは、データベースにMySQLを使用します。このために.envファイルのデータベース関連のパラメーターを次のように変更します。

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>

最後に、お好きなデータベース管理アプリケーションを使用し、新しいデータベースorder_apiを作成します。

データベース初期データの作成

注文管理アプリケーションを作成するために、次のコマンドを実行してモデルを作成します。

php artisan make:model Order -a

-a引数により、ArtisanにOrderモデルのマイグレーションファイル、シーダー、ファクトリー、コントローラーを作成することを指示します。

上記のコマンドにより5つの新しいファイルが作成されます。

  • コントローラー - app/Http/Controllers/OrderController.php
  • データベースファクトリー - database/factories/orderFactory.php
  • マイグレーションファイル - database/migrations/YYYY_MM_DD_HHMMSS_create_orders_table.php
  • モデル - app/Models/Order.php
  • シーダーファイル - database/seeders/OrderSeeder.php

database/migrations/YYYY_MM_DD_HHMMSS_create_orders_table.phpのup関数を次のとおり変更します。

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();
    });
 }

マイグレーションファイルに従い、orderテーブルには次の列が含まれます。

  1. ID。テーブルの1次キーになります。
  2. 注文の詳細。
  3. 発注したクライアントの名前。
  4. 注文処理の完了と未了。
  5. 注文の作成と更新時のcreated_atupdated_attimestamps関数により提供)。

次にOrderFactoryを更新し、データベースに入力するダミーの注文を作成します。database/factories/OrderFactory.phpdefinition関数を次のとおり変更します。

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

次に、database/seeders/OrderSeeder.phpを開き、run関数を次のとおり変更します。

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

これにより、OrderFactoryはデータベースに50個の注文を作成します。

Don't forget to add this import:

use AppModelsOrder;

src/database/seeders/DatabaseSeeder.phprun関数に以下を追加します。

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

これにより、Artisanのdb:seedコマンドを実行したときにQuoteSeederが実行されます。

最後に、マイグレーションを実行し、次のコマンドでデータベースにデータを格納します。

php artisan migrate --seed

これでordersテーブルを開くと、新しく追加された注文が含まれています。

注文リスト

リポジトリの作成

Orderモデルのリポジトリを作成する前に、リポジトリによる宣言が必要なすべてのメソッドを指定するインターフェイスを定義します。直接リポジトリクラスを使う代わりに、コントローラー(と今後作成する注文コンポーネント)はこのインターフェイスを使用して操作を行います。

これにより、今後変更の必要がある場合にコントローラーは影響を受けず、柔軟なコードを作成できます。例えば、注文管理をサードパーティアプリケーションにアウトソースする場合、OrderRepositoryInterfaceの署名に準拠する新しいモデルを作成してバインディング宣言を置き換えれば、コントローラーのコードを一行も変更することなく、コントローラーが正しく機能します。

appディレクトリに新しいフォルダInterfacesを作成します。続いて、Interfacesに新しいファイルOrderRepositoryInterface.phpを作成して次のコードを追加します。

<?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();
}

次に、appフォルダに新しいフォルダRepositoriesを作成します。このフォルダに新しいファイルOrderRepository.phpを作成して次のコードを追加します。

<?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);
    }
}

インターフェイスが持つ柔軟性とは別に、このようにクエリーをカプセル化することにより、アプリケーション全体でクエリーの重複を避けられるメリットも得られます。

今後、getAllOrders()関数で受注処理が未了の注文のみを取得することがあれば、変更は一カ所だけでよいため、Order::all()が宣言されたすべての箇所を探す必要はなく、見落とすリスクが避けられます。

コントローラーの作成

リポジトリができたところで、コントローラーにコードを追加します。app/Http/Controllers/OrderController.phpを開き、コードを次のように変更します。

<?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);
    }
}

このコードにより、コンストラクタからOrderRepositoryInterfaceインスタンスが注入され、それぞれのコントローラーメソッドで関連オブジェクトのメソッドが使用されます。

まず、index()メソッドがorderRepositoryに定義されたgetAllOrders()メソッドを呼び出し、注文のリストを取得してJSON形式の応答を返します。

次にstore()メソッドがorderRepositoryからcreateOrder()メソッドを呼び出し、新しい注文を作成します。配列として作成する注文詳細が取得され、成功すると後で応答が返されます。

コントローラーのshow()メソッドには、ルートから一意の注文Idが取得され、パラメーターとしてgetOrderById()に渡されます。これにより、データベースからIdの一致する注文の詳細が取得され、JSON形式の応答が返されます。

続いて、作成済みの注文の詳細を更新するために、リポジトリからupdateOrder()メソッドが呼び出されます。これにより、注文の一意のidと更新が必要な詳細の2つのパラメーターを取得します。

最後に、destroy()メソッドがルートから注文の一意のidを取得し、リポジトリからdeleteOrder()メソッドを呼び出してこれを削除します。

ルートの追加

コントローラーに定義された各メソッドを個々のルートにマップするために、routes/api.phpに次のコードを追加します。

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;

インターフェイスと実装のバインド

最後に、OrderRepositoryLaravelサービスコンテナOrderRepositoryInterfaceにバインドする必要があります。これは、サービスプロバイダーを使用して行います。次のコマンドを使用して作成します。

php artisan make:provider RepositoryServiceProvider

app/Providers/RepositoryServiceProvider.phpを開き、register関数を次のように変更します。

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;

最後に、config/app.phpproviders配列に新しいサービスプロバイダーを追加します。

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

アプリケーションのテスト

次のコマンドを使用してアプリケーションを実行します。

php artisan serve

デフォルトで、ターゲットのアプリケーションはhttp://127.0.0.1:8000/を使用します。PostmancURLを使用し、新しく作成したAPIにリクエストを実行します。

次のコマンドを実行し、cURLを使用して/api/ordersエンドポイントをテストします。

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

The response was formatted to JSON using jq.

ターミナルに次のようなJSONの出力が表示されます。例は、見やすいように右端が切断してあります。

{
  "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"
    }
  ]
}

Laravelアプリケーションのリポジトリパターンの使い方の紹介の終わりに

この記事では、リポジトリパターンについて、Laravelアプリケーションでの使い方を紹介しました。大規模プロジェクトにおけるいくつかのメリットとして、具体化した実装でなく疎結合されたコードによる抽象化があることも見ていただきました。

一つ最後に注意を記しておきます。小規模のプロジェクトでは、このアプローチは多くの作業が必要で、複数の場所で繰り返される定型コード(ボイラープレートコード)が発生し、すぐにメリットが得られないように感じます。このため、このアプローチをとる前に、プロジェクトの規模を適切に考慮する必要があります。

チュートリアルで使用したすべてのコードベースはGitHubで入手できます。いろいろ調べてみてください。コーディングを楽しみましょう!

Oluyemi氏は、電気通信工学のバックグラウンドを持つ技術愛好家です。ユーザーが直面する日々の問題を解決することに強い関心を持ち、プログラミングの道に進んで以来、Webとモバイルの両方のソフトウェア開発で問題解決能力を磨いてきました。

Oluyemi氏は、知識の共有に情熱を注ぐフルスタックのソフトウェアエンジニアであり、いくつかのブログで多数の技術記事とコンテンツをインターネットに公開しています。技術にも精通しており、趣味は新しいプログラミング言語とフレームワークを試すことです。