Menu
Coddy logo textTech

リポジトリパターン

CoddyのPHPジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 81/91。

リポジトリパターンは、アプリケーションのビジネスロジックとデータストレージの間を仲介するデザインパターンです。これは、ドメインオブジェクトにアクセスするためのコレクションのようなインターフェースを提供し、データが実際にどのように保存または取得されるかという詳細を隠蔽します。

リポジトリを、オブジェクトの永続化や取得の方法を熟知した特別なコレクションと考えてください。アプリケーションコードは、データがデータベース、ファイル、あるいはAPIのどこに保存されているかを意識することなく、リポジトリに対してIDによるユーザーの取得や新しい製品の保存を依頼します。この分離により、コードのテストが容易になり、柔軟性が高まります。

<?php
interface UserRepositoryInterface {
    public function find(int $id): ?User;
    public function findAll(): array;
    public function save(User $user): void;
    public function delete(int $id): void;
}

class User {
    public function __construct(
        public int $id,
        public string $name,
        public string $email
    ) {}
}

リポジトリの実装は、実際のストレージメカニズムを処理します:

<?php
class InMemoryUserRepository implements UserRepositoryInterface {
    private array $users = [];
    
    public function find(int $id): ?User {
        return $this->users[$id] ?? null;
    }
    
    public function findAll(): array {
        return array_values($this->users);
    }
    
    public function save(User $user): void {
        $this->users[$user->id] = $user;
    }
    
    public function delete(int $id): void {
        unset($this->users[$id]);
    }
}

$repo = new InMemoryUserRepository();
$repo->save(new User(1, "Alice", "alice@example.com"));
$repo->save(new User(2, "Bob", "bob@example.com"));

echo $repo->find(1)->name . "\n";
echo count($repo->findAll());

出力:

Alice
2

このパターンの利点は、ビジネスロジックを変更することなく実装を入れ替えられることです。インメモリ・ストレージからデータベースに切り替える必要がありますか?その場合は、同じインターフェースを実装する新しいリポジトリクラスを作成するだけです。アプリケーションコードは具体的な実装ではなくインターフェースに依存しているため、コードに手を加える必要はありません。

challenge icon

チャレンジ

簡単

リポジトリパターンを使用して、製品在庫システムを構築しましょう。ドメインオブジェクトとそれらの保存方法を明確に分離することで、アプリケーションコードがストレージの詳細を知らなくても製品を操作できるようにします。

コードは4つのファイルに分けて構成します:

  • Product.php — 在庫内のアイテムを表す Product クラスを作成します。各製品は id (int)、name (string)、および price (float) を持ちます。簡単にアクセスできるように、パブリックプロパティを持つコンストラクタのプロパティ昇格(constructor promotion)を使用してください。
  • ProductRepositoryInterface.php — 製品ストレージの実装のための規約を確立する ProductRepositoryInterface を定義します。インターフェースでは以下のメソッドを宣言する必要があります:
    • find(int $id): ?Product — IDで製品を取得し、見つからない場合は null を返します
    • findAll(): array — すべての製品を配列として取得します
    • save(Product $product): void — 製品を保存または更新します
    • delete(int $id): void — IDで製品を削除します
  • InMemoryProductRepository.php — Productクラスとインターフェースの両方をインクルードします。製品を保存するためにプライベート配列を使用し、インターフェースを実装する InMemoryProductRepository クラスを作成します。効率的な検索のために、製品はIDをインデックスとして保存する必要があります。findAll() メソッドは製品オブジェクトのみを返すようにしてください(配列のキーをリセットするために array_values() を使用します)。
  • main.php — リポジトリファイルをインクルードします。入力として、追加する製品の配列を含むJSON文字列と、検索するIDの2つを受け取ります。

    JSONの形式は以下の通りです:

    [{"id": 1, "name": "Laptop", "price": 999.99}, {"id": 2, "name": "Mouse", "price": 29.99}]

    InMemoryProductRepository を作成し、JSON入力からすべての製品を保存した後、2番目の入力(ID)を使用して特定の製品を検索します。結果を以下の形式で出力してください:

    Total products: [count]
    Found: [name] - $[price]

    価格は小数点以下2桁までフォーマットしてください。製品が見つからない場合は、代わりに Found: Not found と出力してください。

リポジトリパターンは、ストレージの実装を入れ替える必要があるときに真価を発揮します。メインのコードはインターフェースにのみ依存しているため、メモリ内ストレージからデータベースに切り替える場合でも、アプリケーションのロジックを変更することなく、新しいリポジトリクラスを作成するだけで済みます。

チートシート

リポジトリパターンは、ビジネスロジックとデータストレージの間を仲介し、ストレージの実装の詳細を隠しながら、ドメインオブジェクトにアクセスするためのコレクションのようなインターフェースを提供します。

データアクセスのための規約を確立するリポジトリインターフェースを定義します:

<?php
interface UserRepositoryInterface {
    public function find(int $id): ?User;
    public function findAll(): array;
    public function save(User $user): void;
    public function delete(int $id): void;
}

コンストラクタのプロパティ昇格(constructor promotion)を使用してドメインクラスを作成します:

<?php
class User {
    public function __construct(
        public int $id,
        public string $name,
        public string $email
    ) {}
}

具体的なストレージメカニズムを使用してリポジトリインターフェースを実装します:

<?php
class InMemoryUserRepository implements UserRepositoryInterface {
    private array $users = [];
    
    public function find(int $id): ?User {
        return $this->users[$id] ?? null;
    }
    
    public function findAll(): array {
        return array_values($this->users);
    }
    
    public function save(User $user): void {
        $this->users[$user->id] = $user;
    }
    
    public function delete(int $id): void {
        unset($this->users[$id]);
    }
}

インターフェースを介してリポジトリを使用します:

$repo = new InMemoryUserRepository();
$repo->save(new User(1, "Alice", "alice@example.com"));
$user = $repo->find(1);
$allUsers = $repo->findAll();

このパターンにより、ビジネスロジックを変更することなく実装を入れ替えることができます。つまり、コードは具体的な実装ではなく、インターフェースに依存することになります。

自分で試してみよう

<?php

require_once 'InMemoryProductRepository.php';

// 入力を読み込む
$jsonInput = trim(fgets(STDIN));
$searchId = intval(trim(fgets(STDIN)));

// JSON入力を配列にパースする
$productsData = (array)json_decode($jsonInput, true);

// TODO: InMemoryProductRepositoryのインスタンスを作成する

// TODO: $productsDataをループして各製品を保存する
// 各アイテムは 'id'、'name'、'price' キーを持っています

// TODO: すべての製品を取得し、合計数を出力する
// 形式: "Total products: [count]"

// TODO: $searchIdで製品を検索する
// 見つかった場合、次のように出力します: "Found: [name] - $[price]" (価格は小数点以下2桁にフォーマット)
// 見つからない場合、次のように出力します: "Found: Not found"

?>
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン