本記事ではLaravelのアプリケーションを理解し、より良い設計・アーキテクチャを構築できるように学習したことを簡潔にまとめています。
目次
1.Laravelのアーキテクチャ
2.アプリケーションのアーキテクチャ
3.HTTPリクエストとレスポンス
4.データベース
5.認証と許可
6.イベントとキューによる処理の分離
7.コンソールアプリケーション
8.テスト
9.エラーハンドリングとログの活用
10.テスト駆動開発の実践
4.データベース
4.3 リポジトリパターン
リポジトリパターンとは、ビジネスロジックからデータの保存や復元を別レイヤ(リポジトリ層)へ移し、分離することで、コードのメンテナンス性やテストの容易性を高める実装パターン。
アプリケーションでのデータストア先は様々である。RDBやNoSQLデータベース、あるいはキャッシュやファイル、またはSaasのAPIを利用するケースもある。また、テストコードによる自動テストでは、本番とは違うデータベースを使用する場合もある。
このように、データベースの参照先が変わってもプログラムの変更範囲は可能な限り変えたくない。
→この課題に対応する手段の1つとして、リポジトリパターンがある。
リポジトリパターンでは、ビジネスロジックからデータストアに対して直接操作する処理に切り離し何らかのデータ保存庫(リポジトリ)に対してデータの保存や復元を行う処理を抽象的に扱うオブジェクトを用意する。
リポジトリパターンの実装
出版社テーブル(publishers)へのデータ操作を例に、リポジトリパターンの実装を行っていく。
はじめにサービスクラスとデータベースアクセスが密接に結びついたコードを実装し、そのコードをリファクタリングによって、他のデータストアへの変更やモックの差し替えを容易になるようにリポジトリパターンを採用する。
1. アプリケーション仕様
出版社を新規に追加するWebAPIを作成する。エンドポイントのURIは/api/publishersとする。
2. コードの作成
作成するコードは、下記の3つのファイル。
1. データベースアクセスを受け持つEloquent(Publisher)
2. ビジネスロジックを受け持つサービスクラス(PublisherService)
3. リクエストを受けるコントローラクラス(PublisherAction)
まずは、Eloquentクラスを作成する。DataProvider/Eloquentディレクトリ配下にPublisher.phpを配置する。
<?php
declare(strict_types=1);
namespace App\DataProvider\Eloquent;
use Illuminate\Database\Eloquent\Model;
class Publisher extends Model
{
protected $fillable = [
'name',
'adress',
];
}
続いてサービスクラスの実装を行う。下記コード例に示す通り、appフォルダ配下にServicesフォルダを作成し、PublishService.phpを配置する。
コード例のexistsメソッドでは、引数nameで指定された名前と同じ出版社名がないかを確認し、もし同じ出版社が既に登録されていたら、trueを返却する(①)。また、saveメソッドでは、引数で与えられたnameとaddressをPublisherクラス(Eloquent)のcreateメソッドを使って登録し、登録後のシーケンス値(id)を返す(②)
<?php
declare(strict_types=1);
namespace App\Services;
use App\DataProvider\Eloquent\Publisher;
class PublisherService
{
public function exists(string $name): bool
{
$count = Publisher::whereName($name)->count();
if ($count > 0) {
return true; // ①
}
return false;
}
public function store(string $name, string $address):int
{
$publisher = Publisher::create(
[
'name' => $name,
'address' => $address,
]);
return (int)$publisher->id;
}
}
続いてコントローラクラスを実装する。app/Http/Controllersディレクトリに、PublisherAction.phpの名前で作成する。
ユーザーリクエストからのnameとaddressを受け取って、nameで指定された名前と同じ出版社名が存在しないかの確認を行う(Serviceクラス)。同一出版社名が既に登録済みの場合は何も行わず、HTTPステータス200で返却し、(①)登録されていない場合は、新規登録を行い、HTTPステータス201を返す(②)。
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Services\PublisherService;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class PublisherAction
{
private $publisher;
public function __construct(PublisherService $publisher)
{
$this->publisher = $publisher;
}
public function create(Request $request)
{
if ($this->publisher->exists($request->name)) {
return response('', Response::HTTP_OK); // ①
}
$id = $this->publisher->store($request->name, $request->address);
return response('', Response::HTTP_CREATED)->header('Location', 'api/publisher/'.$id);
}
}
最後にエンドポイントをroutes/api.phpに登録する。
Route::post('/publishers', [App\Http\Controllers\PublisherAction::class, 'create']);
動作を確認するために、curlコマンドを実行し、データベースの中身を確認する。
$ curl 'http://localhost/api/publishers'\
--request POST\
--data 'name=テスト出版社&address=東京都千代田区神田',
リファクタリング
前述のPublishServiceクラスを改めて確認すると、データの存在確認や登録処理はEloquentに依存しているため、データベースの代わりにモックやEloquent以外のデータ操作クラスを利用しようとすると、サービスクラスを大幅に修正しなければならない。
そこで下記の手順に従い、ビジネスロジックから特定のデータベース操作を取り除く。
1.Repositoryを抽象化するインターフェースとEntityクラスを作成する。
2.データベース操作を担当するRepositoryクラスを作成する
3.Serviceクラスはインターフェースを参照する
4.インターフェースと具象クラスを紐づける
1.レポジトリとEntityクラスの作成
データ操作をServiceクラスから見た場合、Publisherオブジェクトは、同名出版社の存在確認と登録処理ができればよいため、この2つの処理をもつクラスを「リポジトリ」として新たに定義する。同時に処理を抽象化し、これを表現したクラスをインターフェースとして作成する。
下記にリポジトリインターフェースを示す。
<?php
declare(strict_types=1);
namespace App\DatabaseProvider;
use App\Domain\Entity\Publisher;
interface PublisherRepositoryInterface
{
public function findByName(string $name): ?Publisher; // ①
public function store(Publisher $publisher): int; // ②
}
appディレクトリ内のDataProviderは以下に、PublisherRepositoryInterface.phpを作成する。インターフェースクラスであるため、出版社名をキーにデータ取得を行うfindByName(①)と登録処理を行うstore(②)のメソッド定義のみを行う。
次に、メソッドの戻り値や引数として指定するEntityクラスも合わせて作成する。
<?php
declare(strict_types=1);
namespace App\Domain\Entity;
class Publisher
{
protected $id;
protected $name;
protected $address;
public function __construct(int $id, string $name, string $address)
{
$this->id = $id;
$this->name = $name;
$this->address = $address;
}
public function getName():string
{
return $this->name;
}
public function getAddress():string
{
return $this->address;
}
}
2.データベース操作を担当するRepositoryクラスを作成する
続いて、上記のインターフェースの実処理を行う具象クラス(コンクリートクラス)を作成する。PublisherActionクラスで実行していたデータアクセス処理をこちらに移動する。
下記にリポジトリインターフェースを実装した具象クラスを示す。
<?php
declare(strict_types=1);
namespace App\Domain\Repository;
use \App\DataProvider\PublisherRepositoryInterface;
use \App\DataProvider\Eloquent\Publisher as EloquentPublisher;
use \App\Domain\Entity\Publisher;
class PublisherRepository implements PublisherRepositoryInterface
{
private $eloquentPublisher;
pubilc function __construct(EloquentPublisher $eloquentPublisher)
{
$this->eloquentPublisher = $eloquentPublisher;
}
public function findByName(string $name): ?Publisher
{
$record = $this->eloquentPublisher->wherenName($name)->first();
if ($record === null) {
return null;
}
return new Publisher(
$record->id,
$record->name,
$record->address,
);
}
public function store(Publisher $publisher):int
{
$eloquent = $this->eloquentPublisher->newInstance();
$eloquent->name = $publisher->getName();
$eloquent->address = $publisher->getAddress();
$eloquent->save();
return (int)$eloquent->id;
}
}
上記のコードでは、app\Domain\Repository配下にPublisherRepository.phpを作成し、コンストラクタインジェクションで、データストアへのアクセスを行うEloquentPublisherクラスを注入する。
3.Serviceクラスはインターフェースを参照する
ここまでの実装で、データ操作の実処理はリポジトリクラスに移った。次にPublisherServiceクラスでは、MySQLのデータアクセスクラス(\App\DataProvider\Eloquent\Publisher)を直接利用していたので、抽象クラスであるPublisherRepositoryInterfaceをコンストラクタインジェクションで引数として渡す形式に変更する。
下記に、サービスクラスのリファクタリングを示す。
<?php
declare(strict_types=1);
namespace App\Services;
use App\DataProvider\PublisherRepositoryInterface;
use App\Domain\Entity\Publisher;
class PublisherService
{
private $publisher;
public function __construct(PublisherRepositotyInterface $publisher)
{
$this->publisher = $publisher;
}
public function exists(string $name): bool
{
if (!$this->publisher->findByName($name)) {
return false;
}
return true;
}
public function store(string $name, string $address): int
{
return $this->publisher->store(new Publisher(null, $name, $address));
}
}
上記コード例のサービスクラスは、同じPublisherRepositoryInterfaceインターフェースを持つクラスであれば何でも動作するようになった。
ユニットテストでは、モッククラスを利用可能であり、また、他のデータストアを利用することになっても、サービスクラスには変更は必要ない。コントローラもこのサービスクラスを利用するので、データストア先が変わっても影響を受けないようになっている。
4.インターフェースと具象クラスを紐づける
最後に、インターフェースと具象クラスの関連付け(バインディング)を忘れずに行う。サービスプロバイダクラスのregisterメソッドに追記する。(次の例では、既存のApp\Providers\AppServiceProvierクラスに登録するが、新しく作成してもよい)
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(
\App\DataProvider\PublisherRepositoryInterface::class,
\App\DataProvider\PublisherRepository::class,
);
}
前述のcurlコマンドを実行すると、同様にデータを登録できる。もし、データストア先を変更する場合、PublisherRepositoryInterfaceを持ったデータ操作クラスを新たに作成し、バインド定義しなおせば、ビジネスロジックを変更することなく、データ操作処理のみを差し替えることができる。
最後に
リポジトリパターンは各クラスが疎結合にできる反面、クラス数が増えるため、でもプログラムや短期間で使用するプログラムでは不要になるかもしれない。しかし、システムの要件や規模の拡張が見込まれるサービスでは、検討に値するデザインパターンである。