LaravelでRepositoryパターンを実装する-実践編-
- 公開日
- 更新日
- カテゴリ:Laravel
- タグ:PHP,Laravel,Repository,Architecture

前回の記事 で、Laravel でのリポジトリパターンについて、その基本動作について書きました。 今回はより実践的な使い方で Repository パターンを実装していきます。
Contents
開発環境
今回の開発環境については以下の通りです。
- Laravel 5.8
- MySQL 8.0
- SQLite 3.7
Laravel のルートディレクトリを「laravel/」としています。
SQLite の導入についてはLaravel と SQLite を用いた開発環境とデータソースを用意する を参照してください。
設計
今顔は「書籍の一覧を返す」という動作を、Repository パターンを使って実装していきます。 まずは大まかな流れや条件などを考え、必要なクラスなどをプチ設計しておきます。
- 書籍のリストを返却する
-
- 単体の書籍クラス(Book)
- 複数の書籍クラスから成る書籍リストクラス(BookList)
書籍を司るサービスクラスが一連の操作(ビジネスロジック)を担う(BookService)
ストレージは2つ。これを Repository パターンで切り替え可能にする。
- MySQL(BookMysqlRepository)
- SQLite(BookSqliteRepository)
データ構造について books テーブルと authors テーブルがあり、リスト取得の際に author の名前を結合して取得する こんなところでしょうか。これを基にして実装を行っていきます。
尚、ディレクトリの構造については各々のポリシーの上自由に配置してもらえればと思うのでここでは特に拘って設置はしていません。
実装
それでは実装していきます。まずは、リポジトリパターンの前に細々したものを定義していきます。
最小単位である、単体の「書籍」を表現する書籍クラスです。
- laravel/app/Entities/Book.php
-
<?php declare(strict_types=1); namespace App\Entities; class Book { protected $id; protected $title; protected $author; public function __construct(int $id, string $name, string $author) { $this->id = $id; $this->title = $name; $this->author = $author; } public function getId(): int { return $this->id; } public function getTitle(): string { return $this->title; } public function getAuthor(): string { return $this->author; } }
書籍リストを表現する書籍リストクラスです。
- laravel/app/Entities/BookList.php
-
<?php declare(strict_types=1); namespace App\Entities; class BookList implements \IteratorAggregate { private $bookList; public function __construct() { $this->bookList = new \ArrayObject(); } public function add(Book $book) { $this->bookList[] = $book; } public function getIterator(): \ArrayIterator { return $this->bookList->getIterator(); } }
リストを取得したらループで回したいので IteratorAggregate インターフェースを実装しています。 getIterator() で ArrayIterator オブジェクトを返却しています。
Eloquent を使用するのでモデルを定義します。
- laravel/app/Models/Book.php
-
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Book extends Model { public $timestamps = false; public function author() { return $this->belongsTo('App\Models\Author'); } }
Author モデルに対して多対1のリレーションを定義しています。
- laravel/app/Models/Author.php
-
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Author extends Model { public $timestamps = false; public function book() { return $this->hasMany('App\Models\Book'); } }
BooK モデルに対して1対多のリレーションを定義しています。
リポジトリパターンの実装
ここから、リポジトリパターンの実装になります。 まず最初に、「書籍を司るサービスクラスが一連の操作(ビジネスロジック)を担う」と定義しましたが、 このサービスクラスにリポジトリの仕様が追従出来るように、インターフェースを定義します。
- laravel/app/Services/BookDataAccess.php
-
<?php declare(strict_types=1); namespace App\Services; interface BookDataAccess { public function getList(); }
「Book のリポジトリとして認めてもらいたければこの実装を遵守する」と高らかに宣言している。 そんなインターフェースになります。
次に、MySQL と SQLite それぞれのリポジトリの具象クラスを定義していきます。
- laravel/app/Repositories/BookMysqlRepository.php
-
<?php declare(strict_types=1); namespace App\Repositories; use App\Services\BookDataAccess; use App\Models\Book AS BookModel; use App\Entities\Book; use App\Entities\BookList; class BookMysqlRepository implements BookDataAccess { protected $BookModel; protected $BookList; private $connection = 'mysql'; public function __construct(BookModel $BookModel, BookList $BookList) { $this->BookModel = $BookModel; $this->BookList = $BookList; } public function getList(): BookList { $data = $this->BookModel::on($this->connection)->with('author:id,name')->get(); foreach ($data as $d) { $this->BookList->add(new Book($d->id, $d->name, $d->author->name)); } return $this->BookList; } }
- laravel/app/Repositories/BookSqliteRepository.php
-
<?php declare(strict_types=1); namespace App\Repositories; use App\Services\BookDataAccess; use App\Models\Book AS BookModel; use App\Entities\Book; use App\Entities\BookList; class BookSqliteRepository implements BookDataAccess { protected $BookModel; protected $BookList; private $connection = 'sqlite'; public function __construct(BookModel $BookModel, BookList $BookList) { $this->BookModel = $BookModel; $this->BookList = $BookList; } public function getList(): BookList { $data = $this->BookModel::on($this->connection)->with('author:id,name')->get(); foreach ($data as $d) { $this->BookList->add(new Book($d->id, $d->name, $d->author->name)); } return $this->BookList; } }
2つのリポジトリクラスは、各々 BookDataAccess インターフェースを実装している点がポイントです。 彼らは Book のリポジトリとして認めてもらいたいので、上層の仕様に忠実に従っています。
結果論ですが実はこの2つのクラス、connection メンバ変数以外は記述が全く同じです。 なのでまとめてしまっても良いかなと思います。今回はわかりやすいように別々のままにしています。
リポジトリクラスが作成できたら、これらをサービスプロバイダへ登録し切り替えられるようにします。
- laravel/app/Providers/RepositoryServiceProvider.php
-
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; class RepositoryServiceProvider extends ServiceProvider { /** * Register services. * * @return void */ public function register() { $this->app->bind(\App\Services\BookDataAccess::class, function($app) { // MySQL リポジトリを使用する場合はこちら return new \App\Repositories\BookMysqlRepository(new \App\Models\Book, new \App\Entities\BookList); // SQLite リポジトリを使用する場合はこちら //return new \App\Repositories\BookSqliteRepository(new \App\Models\Book, new \App\Entities\BookList); }); } /** * Bootstrap services. * * @return void */ public function boot() { // } }
laravel/config/app.php
'providers' => [
// 追加
App\Providers\RepositoryServiceProvider::class,
],
リポジトリの利用
一連の実装が完了したので、これらを利用していきます。
まずはリポジトリを利用するサービスクラスです。
- laravel/app/Services/BookService.php
-
<?php declare(strict_types=1); namespace App\Services; class BookService { protected $BookDataAccess; public function __construct(BookDataAccess $BookDataAccess) { $this->BookDataAccess = $BookDataAccess; } public function getList() { return $this->BookDataAccess->getList(); } }
リポジトリをコンストラクタインジェクションにて注入しています。 サービスプロバイダでアクティブにしたリポジトリがここで自動的に解決されて注入されます。
最後に、コントローラからサービスクラスを使用します。
- laravel/app/Http/Controllers/BookListController.php
-
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Services\BookService; class BookListController extends Controller { private $BookService; public function __construct(BookService $BookService) { $this->BookService = $BookService; } public function index() { // データ取得 $list = $this->BookService->getList(); /* ループ例 foreach ($list->getIterator() as $d) { echo $d->getTitle(); } */ /* ビューへ投げたり return view('book_list', ['list' => $list]); */ } }
取得したリストをループで処理したり、ビューに投げたりはおまかせです。
ルーティングも簡単に
- laravel/routes/web.php
-
Route::get('/', 'BookListController@index');
動作確認
すべての実装が完了したので、ブラウザからアクセスして出力を確認してみます。まずは、MySQL リポジトリを使用した場合です。
[0] => App\Entities\Book Object
(
[id:protected] => 1
[title:protected] => Natus et ipsam tempora tempora.
[author:protected] => 井高 美加子
)
[1] => App\Entities\Book Object
(
[id:protected] => 2
[title:protected] => Corrupti temporibus praesentium mollitia neque impedit similique velit.
[author:protected] => 藤本 春香
)
[2] => App\Entities\Book Object
(
[id:protected] => 3
[title:protected] => Eveniet fuga occaecati nulla.
[author:protected] => 井高 美加子
)
次に、サービスプロバイダから使用するリポジトリを SQLite リポジトリへ切り替えて再度出力してみます。
- laravel/app/Providers/RepositoryServiceProvider.php
-
public function register() { $this->app->bind(\App\Services\BookDataAccess::class, function($app) { // MySQL リポジトリを使用する場合はこちら //return new \App\Repositories\BookMysqlRepository(new \App\Models\Book, new \App\Entities\BookList); // SQLite リポジトリを使用する場合はこちら return new \App\Repositories\BookSqliteRepository(new \App\Models\Book, new \App\Entities\BookList); }); }
出力結果は以下になります。
[0] => App\Entities\Book Object
(
[id:protected] => 1
[title:protected] => Et blanditiis dolorem animi debitis tempora ad quia.
[author:protected] => 小林 桃子
)
[1] => App\Entities\Book Object
(
[id:protected] => 2
[title:protected] => Dolor aliquam incidunt qui saepe fuga.
[author:protected] => 小林 裕樹
)
[2] => App\Entities\Book Object
(
[id:protected] => 3
[title:protected] => Eos sit possimus blanditiis facere est.
[author:protected] => 佐々木 真綾
)
データソースが切り替わり、それぞれからデータを取得できた事が確認できました。
まとめ
以上で作業は終了です。 リポジトリパターンを実装する際には依存の方向を逆転させ、あくまでも下層が上層に従うようにしていく(上層に配置したインターフェースに下層のリポジトリが従う)事がポイントです。
また、どちらのリポジトリを使用しても同じ処理で流れていけるように、返されるもののフォーマットもしっかり揃えていく事も大切(双方のリポジトリで返ってくる形式や項目が違うのではそもそも使えない)です。 是非試してみてください。