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

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

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

この記事はOluyemi Olususiこちらで公開した記事(英語)を日本語化したものです。

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

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.phpup関数を次のとおり変更します。

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。テーブルのプライマリーキーとなります。
  2. 注文の詳細。
  3. 発注した顧客の名前。
  4. 注文処理が完了か未了かを知らせるステータス。
  5. 注文の作成日時と更新日時。timestamps関数により提供されるcreated_atupdated_atが使用されます。

次に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件の注文を作成します。

Orderをインポートしてください。

use AppModelsOrder;

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

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

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

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

php artisan migrate --seed

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

Ordersテーブル

リポジトリの作成

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

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

appディレクトリにInterfacesディレクトリを新規作成します。続いて、InterfacesOrderRepositoryInterface.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ディレクトリを新規作成します。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()メソッドは、APIルートから一意の注文Idを取得し、パラメーターとしてgetOrderById()に渡します。これにより、データベースからIDの一致する注文の詳細が取得され、JSON形式のレスポンスが返されます。

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

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

APIルートの追加

コントローラーに定義された各メソッドを個々のAPIルートにマップするために、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']);

OrderControllerをインポートしてください。

use AppHttpControllersOrderController;

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

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

php artisan make:provider RepositoryServiceProvider

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

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

OrderRepositoryOrderRepositoryInterfaceをインポートしてください。

use AppInterfacesOrderRepositoryInterface;
use AppRepositoriesOrderRepository;

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

'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

レスポンスはjqを使用してJSON形式に整形されています。

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