今回はLaravelでリポジトリパターンのベースを作成していきたいと思います。
前回のLaravelの記事から4ヶ月くらい経ってしまい、Laravelもバージョン7になってしまいましたが、基本的に大きく作り方は変わらないかと思います。
今回のサンプルはPHP7.4をベースに作成しています。
プロパティの型指定などをしていますが、PHP7.4未満の場合は適宜書き換えてください。
リポジトリパターンとは
リポジトリパターンは、リポジトリを使用する側がデータのアクセス部分を気にする事なく利用できるようにする方法です。
リポジトリクラスを利用する側は、データのI/Oさえわかればデータの取得元はDBでも外部APIでもKVSでも気にする必要がなくなります。
これにより、取得元のデータに変更があった場合、修正範囲を極力小さくすることが出来ます。
リポジトリパターンの実装
今回は極力シンプルに最小限のコードでサンプルを作成します。
Modelの作成
まず、サンプルのModelを2パターン作成します。
自分はModelを1つのフォルダに纏めたいので、Modelsのフォルダを作成して配置します。
app/Models/SampleA.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SampleA extends Model
{
//
static function getData(): array
{
return [
'from-date' => '2020-01-01',
'to-date' => '2020-01-31',
'price' => 10000,
];
}
}
app/Models/SampleB.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SampleB extends Model
{
static function getData(): array
{
return [
'from' => '2020-12-01',
'to' => '2020-12-31',
'price' => 50000
];
}
}
今回は、期間の開始日付と終了日付と金額をデータとして返しますが、サンプルとして同じようなデータでもKeyが一部違うものを用意します。
データ格納用クラスの作成
それぞれのModelより取得してきたデータを格納するためのクラスを作成します。自分もまだまだ勉強中なので、詳しい解説は出来ませんが、DDDの値オブジェクトに近いものになります。
このクラスを用意する事で、各リソースでElooquentだったりJsonだったりそれぞれ異なる形式を吸収します。
app/Value/SampleDataValue.php
namespace App\Value;
class SampleDataValue
{
private string $fromDate;
private string $toDate;
private int $price;
/**
* @param string $fromDate
*/
public function setFromDate(string $fromDate): void
{
$this->fromDate = $fromDate;
}
/**
* @param int $price
*/
public function setPrice(int $price): void
{
$this->price = $price;
}
/**
* @param string $toDate
*/
public function setToDate(string $toDate): void
{
$this->toDate = $toDate;
}
/**
* @return string
*/
public function getFromDate(): string
{
return $this->fromDate;
}
/**
* @return int
*/
public function getPrice(): int
{
return $this->price;
}
/**
* @return string
*/
public function getToDate(): string
{
return $this->toDate;
}
}
Repositoryの作成
Modelからリソースを取得するレポジトリクラスを作成します。
まずは、I/Oを確定させるためにインターフェースを作成します。
app/Repositories/SampleRepositoryInterface.php
namespace App\Repositories;
use App\Value\SampleDataValue;
interface SampleRepositoryInterface
{
public function getData(): SampleDataValue;
}
単純なデータを取得するメソッドを一つ作成します。
次に各Modelに対応したRepositoryクラスを作成します。
app/Repositories/SampleARepository.php
namespace App\Repositories;
use App\Value\SampleDataValue;
interface SampleRepositoryInterface
{
public function getData(): SampleDataValue;
}
app/Repositories/SampleBRepository.php
namespace App\Repositories;
use App\Models\SampleB;
use App\Value\SampleDataValue;
class SampleBRepository implements SampleRepositoryInterface
{
public function getData(): SampleDataValue
{
$data = SampleB::getData();
$value = new SampleDataValue();
$value->setFromDate($data['from']);
$value->setToDate($data['to']);
$value->setPrice($data['price']);
return $value;
}
}
このクラスで各Modelの違いを吸収し、SampleDataValueに格納して返してあげることにより、サービスクラスはリソース元に影響されなくなります。
LaravelだとEloquentなどでModelから値をもらう事があると思いますが、Modelに変更がありEloquentではなくなってしまった場合、修正がサービスクラスにまで及びます。
このような状況を回避するために、クラスを用意して返すようにします。
Serviceの作成
サービスクラスはControllerから呼ばれ、必要なRepositoryを利用します。
コンストラクタで使用するRepositoryのInterfaceを指定します。
app/Services/SampleDataService.php
namespace App\Services;
use App\Repositories\SampleRepositoryInterface;
class SampleDataService
{
private SampleRepositoryInterface $sampleRepository;
public function __construct(SampleRepositoryInterface $sampleRepository)
{
$this->sampleRepository = $sampleRepository;
}
public function get()
{
return $this->sampleRepository->getData();
}
}
RepositoryServiceProviderの作成
RepositoryとRepositoryInterfaceを紐づけるため、Providerを作成します。
app/Providers/RepositoryServiceProvider.php
namespace App\Providers;
use App\Repositories\SampleARepository;
use App\Repositories\SampleBRepository;
use App\Repositories\SampleRepositoryInterface;
use Illuminate\Support\ServiceProvider;
class RepositoryServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
//
$this->app->bind(SampleRepositoryInterface::class, SampleARepository::class);
// 切り替える
// $this->app->bind(SampleRepositoryInterface::class, SampleBRepository::class);
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
//
}
}
21行目をコメントアウトしていますが、ここで作成したSampleRepositoryInterfaceをどのレポジトリと紐づけるかを定義します。
このサンプルでは、SampleARepositoryと紐づけていますが、SampleBRepositoryと紐づければ、SampleBのModelからデータが取れるようになります。
実用例としては、envから環境を取得し開発環境とその他環境で読み込むRepositoryを変えることも可能です。
RepositoryServiceProviderの登録
作成したRepositoryServiceProviderを登録します。
providerの配列に下記を追加してください。
config/app.php
App\Providers\RepositoryServiceProvider::class,
Controllerの作成
SampleDataServiceを呼び出すControllerを作成します。
今回は簡単にddで画面出力します。
app/Http/Controllers/SampleController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\SampleDataService;
class SampleController extends Controller
{
private SampleDataService $sampleDataService;
public function __construct(SampleDataService $sampleDataService)
{
$this->sampleDataService = $sampleDataService;
}
public function index()
{
$sampleData = $this->sampleDataService->get();
dd($sampleData);
}
}
Routeの設定
Contorollerを呼び出すため、Routeの設定をします。
この辺りはサンプルなので、簡単にルートにアクセスしたら呼び出すようにします。
Route::get('/', 'SampleController@index')->name('sample');
画面の確認
サイトにアクセすると、SampleAのModelのデータが表示されるかと思います。

次にRepositoryServiceProviderを書き換えてSampleBの内容を表示させます。
app/Providers/RepositoryServiceProvider.php
public function register()
{
//
// $this->app->bind(SampleRepositoryInterface::class, SampleARepository::class);
// 切り替える
$this->app->bind(SampleRepositoryInterface::class, SampleBRepository::class);
}
変更したら再度画面をリロードします。

以上でリポジトリパターンのベースが作成出来たかと思います。
今回は一つの値オブジェクトを用意して行いましたが、複数レコードがある場合など様々な考慮が必要なのと、本来は1つの項目に対してそれぞれ値オブジェクトを作成したりするらしいので、また機会があればその辺りも習得して記事をかければと思います。