LaravelでRepositoryパターンを実装する-実践編-
- 公開:
- カテゴリ: PHP Laravel
- タグ: PHP,Laravel,Repository,Architecture
前回の記事で、Laravelでのリポジトリパターンについて、その基本動作について書きました。 今回はより実践的な使い方でRepositoryパターンを実装していきます。
開発環境
今回の開発環境については以下の通りです。
- 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/Book.php
-
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
public $timestamps = false;
public function author()
{
return $this->belongsTo('App\Author');
}
}
Authorモデルに対して多対1のリレーションを定義しています。
- laravel/app/Author.php
-
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Author extends Model
{
public $timestamps = false;
public function book()
{
return $this->hasMany('App\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\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\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\Book, new \App\Entities\BookList);
// SQLite リポジトリを使用する場合はこちら
//return new \App\Repositories\BookSqliteRepository(new \App\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();
}
}
リポジトリをコンストラクタインジェクションにて注入しています。 サービスプロバイダでアクティブにしたリポジトリがここで自動的に解決されて注入されます。
サービスクラスもDIして使うので、これもサービスプロバイダへ登録します。
- laravel/app/Providers/BookServiceProvider.php
-
<?php
namespace App\Providers;
use App\Services\BookService;
use App\Services\BookDataAccess;
use Illuminate\Support\ServiceProvider;
class BookServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
$this->app->bind('BookService', BookService::class);
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
//
}
} - laravel/config/app.php
-
'providers' => [
// 追加
App\Providers\BookServiceProvider::class,
],
最後に、コントローラからサービスクラスを使用します。
- 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\Book, new \App\Entities\BookList);
// SQLite リポジトリを使用する場合はこちら
return new \App\Repositories\BookSqliteRepository(new \App\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] => 佐々木 真綾
)
データソースが切り替わり、それぞれからデータを取得できた事が確認できました。
まとめ
以上で作業は終了です。 リポジトリパターンを実装する際には依存の方向を逆転させ、あくまでも下層が上層に従うようにしていく(上層に配置したインターフェースに下層のリポジトリが従う)事がポイントです。
また、どちらのリポジトリを使用しても同じ処理で流れていけるように、返されるもののフォーマットもしっかり揃えていく事も大切(双方のリポジトリで返ってくる形式や項目が違うのではそもそも使えない)です。 是非試してみてください。