本記事ではLaravelのアプリケーションを理解し、より良い設計・アーキテクチャを構築できるように学習したことを簡潔にまとめています。
#目次
1.Laravelのアーキテクチャ
2.アプリケーションのアーキテクチャ
3.HTTPリクエストとレスポンス
4.データベース
5.認証と許可
6.イベントとキューによる処理の分離
7.コンソールアプリケーション
8.テスト
9.エラーハンドリングとログの活用
10.テスト駆動開発の実践
#2.アプリケーションのアーキテクチャ
##2.1 MVCとADR
laravelにおけるコントローラ
Laravelにおけるリクエストとレスポンスの一連の動作は、Illuminate\Routing\Routerクラスが制御を行っており、コントローラはRouterから指示をもらい実行されるクラスに過ぎない。実際には1つのコントローラでCRUDを表現しないケースも多く、Laravelでは、ルーターからディスパッチされる処理をメソッドとして記述し、そのいくつかをまとめたものをコントローラクラスと呼ぶ。
laravelにおけるモデル
アプリケーション開発では開発者が実装しなければならず、一番頭を悩ませる部分でもある。人によって解釈が異なるため複雑になりやすい部分でもある。
モデルはデータベースを操作する処理を担っていると考える人も多いが、本来はビジネス要求やサービス仕様を模倣し、具象化したもの、つまり、ビジネスロジックを解決する処理グループである。
通常のWebアプリケーションでのモデルは、ビジネスロジックを実装する層と、データベースを操作する層から構成される。
Laravelの場合、データベースを操作する層として、EloquentモデルやQueryBulider機能が提供されている。
<注意>
・Eloquentモデルにデータベース層とビジネスロジックを合わせて実装すると、2層が一緒になるファットモデルとなる。これは、カラム変更やアプリケーション拡大時のデータベースリファクタリングに大きな影響を与え、巨大なクラスになってしまう。
・上記の処理がモデルではなくコントローラに記述されている場合、ファットコントローラとなる。
→
一般的にはモデルではトランザクションスクリプトパターンかドメインモデルパターンのどちらかを採用される
### トランザクションスクリプトパターン
トランザクションスクリプトパターンとはビジネスロジックの一連の処理を1つのクラスにまとめる最もシンプルなパターンである。下記に「書籍を購入する」ビジネスロジックをトランザクションスクリプトパターンで実装した例を示す。
<?php
declare(strict_types=1)
namespace App\Services;
use App\Book;
use App\User;
use App\Purchase;
class BookService
{
protected $user;
public function __construct(User $user)
{
$this->user = $user;
}
//①
public function order(array $books = [])
{
$purchases = [];
foreach ($books as $book) {
//②
if(!$result = Book::find($book->getId())) {
throw new \App\Exception\BoolStockException('在庫エラー');
}
$purchases [] = $result;
}
//③
foreach($purchases as $purchase) {
Purchase::create([
'book_id' => $purchase->id,
'user_id' => $this->user->id,
]);
}
//④
//ポイント加算や決済完了メール送信などの処理
}
<コード解説>
①「書籍を購入する」をorderメソッドで一連の動作を表す。
②書籍購入の一連の流れでデータベースに直接アクセスし、購入できるか調べ、できなければエラーを返す。
③購入データをデータベースに保存。実際はAPIのコールや様々な処理が記述される。
④購入完了後の様々な処理が実際には記述される
今回の例では、単純な実装だったが、アプリケーションの規模が大きくなると、類似する処理を見つけられずに同様の処理が増えてしまい共通化が困難になる。また、似ている処理をクラスとして分割or結合することも多くあり、最終的にはごった煮となったクラスで運用や保守が難しく設計も一貫性を失い、実装内容の把握が困難になる。
処理を簡単に記述できるlaravelの特徴に注力しすぎた実装を行うと、上記の状態を招く。
→開発時に都合がいい実装ではなく、ビジネスロジックをどう解決するかを強く意識して設計する必要がある。
→この問題への対応としてレイヤードアーキテクチャと呼ばれる設計が存在する(次回に解説)
###ドメインモデルパターン
概念整理や分析などを開発の中心におく考え方であり、データベースなどの非機能要件を高著下コードやデータ入出力ありきの考え方とは大きく異なるドメイン駆動設計(Domain-Driven-Design, DDD)である。
##Laravelにおけるビュー
ビューを構成する1つの要素がテンプレート(blade)であり、テンプレート自体はフレームワーク内部でIlluminate\Http\Responseインスタンスを介して出力される。コントローラで返却するインスタンスはBladeテンプレートだけでなく、ビューを返却する際に、特定のHTTPステータスやヘッダーを返却する場合はResponseクラスも利用する。
下記に異なるビューの指定方法を示す。
<?php
declare(strict_types=1)
namespace App\Http\Controllers;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Response;
final class UserController extends Controller
{
public function detail(string $id):View
{
return view('user.detail');
}
public function userDetail(string $id): Response
{
return new Response(view('user.detail'), Response::HTTP_OK);
}
##ADR(Action Domain Responder)
ADRは元来のMVCをさらに洗練させた設計パターンとして提唱されている。
Action:Controller、 Domain:Model、Responder:Viewにそれぞれ対応している。
##アクション(Action)
MVCのコントローラは複数のアクションが含まれており、それぞれのメソッドはGETやPOST、PUTなどに対応している
→個々のメソッドが様々な処理と依存関係にあるため、大規模や仕様変更などを重ねるごとにコントローラクラスが持つ依存関係がメソッドそれぞれで大きく変わり、あるメソッドのみ利用する依存クラスが増える。
それに対してADRのアクションは、1つのアクションにのみ対応させてクラスを独立させ、1つのアクションとルートを対応させる。原則は1つのHTTPメソッド、1つのルートに1つのアクションが対応する。
→複雑化を防ぎ、シンプルにHTTPメソッドを扱い、レスポンダにレスポンス内容の構築を渡せる
下記に、仕様変更により依存関係が追加された実装例を示す(MVC)
<?php
declare(strict_types=1)
namespace App\Http\Controllers;
use App\Service\UserService;
use App\Service\BookReviewService;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
final class UserController extends Controller
{
private $userService;
private $bookReviewService;
// ①
public function __construct(UserService $userService, BookReviewService $bookReviewService) {
$this->userService = $userService;
$this->bookReviewService = $bookReviewService;
}
public function index(Request $request):View
{
return view('user.index', ['user' => $this->userService->retrieveUser($request->get('id'))]);
}
public function store(Request $request):RedirectResponse
{
$this->userService->active($request->get('user_id'), $request->get('user_name'));
// ②
$this->bookReviewService->addReview($request->get('user_id'), $request->get('book_id'), $request->get('review'));
//レスポンス返却の処理
return redirect('/users');
}
}
<コード解説>
①前回のuserControllerに追記し、書籍をレビューするクラスが依存するものとしてコントローラのコンストラクタにBookReviewServiceを追加。
②登録処理が完了後、書籍レビューも同時に行えるようにメソッドが追加されている。一見すると問題ないように見えるが、実はBookReviewServiceクラスはStoreメソッドでのみ利用されており、indexなどの他のメソッドでは利用しないクラスである。
→コントローラクラス内のメソッドのみ利用するクラスが増え続けることで、コントローラクラスが扱う処理が大きくなり、複雑になる
下記にADRパターンを採用することでコードの複雑化を防ぎ独立させる例を示す。
declare(strict_types=1);
namespace App\Http\Actions;
use App\Service\UserService;
use App\Http\Responder\UserResponder;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Http\Controllers\Controller;
final class UserIndexAction extends Controller
{
private $domain;
private $userResponder;
//①
public function __construct(UserService $userService, UserResponder $userResponder)
{
$this->domain = $userService;
$this->userResponder = $userResponder;
}
// ②
public function __invoke(Request $request): Response
{
return $this->userResponder->request($this->domain->retrieveUser($request->get('id'));
}
}
<コード解説>
①コントローラで実装したメソッドをアクションクラスとして独立させ、レスポンダを依存関係として加えた。App\Service\BookReviewServiceクラスは不要なため依存関係から取り除いている。
②レスポンダにドメインの処理結果(ビジネスロジック)を渡し、どのようなレスポンダを返却するかをレスポンダに移譲している。
__invoke()とは
__invoke()とはPHPのマジックメソッドであり、Laravelではこの__invokeメソッドを実装したクラスをルータに登録すると__invokeメソッドがコールされる
Route::get('users', App\Http\Actoins\UserIndexAction::class);
##ドメイン(Domain)
モデルとドメインで大きな違いはない。レスポンダが出力方法を決定するため、ドメインから返却された値を扱うがそれ以上のやり取りはしない。
##レスポンダ(Responder)
・MVCのビューはコントローラを介して変数のみを渡し、コンテンツ以外のHTTPステータスコードなどはコントローラで設定する必要がある。
→モデルから返却された値によって、表示方法の変更、HTTPステータスの変更やクッキーの操作などビジネスロジックを理解したプレゼンテーションロジックがコントローラに含まれる。
・レスポンダは、上記のロジックをコントローラ・アクションから切り離し、コンテンツの情報(変数など)だけでなく、HTTPレスポンスを構築する処理を担当する(HTMLやAPIで利用されるJsonの返却もレスポンダの役割)
下記にレスポンダの実装例を示す。
<?php
declare(strict_types=1);
namespace App\Http\Resonder;
use Illuminate\Http\Response;
use Illuminate\Contracts\View\Factory as ViewFactory;
use App\User as UserModel;
class BookResponder
{
protected $response;
protected $view;
public function __construct(Response $response, ViewFactory $view)
{
$this->response = $response;
$this->view = $view;
}
public function response(UserModel $user):Response
{
if (!$user->id) {
$this->response->setStatusCode(Response::HTTP_NOT_FOUND);
}
$this->response->setContent($this->view->make('user.index', ['user' => $user]);
return $this->response;
}
}
上記のコード例では、ドメインから返却された値を利用し、どのようなレスポンスを返却するかを実装している。
下記にLaravelのヘルパ関数を利用した実装例を示す。
public function response(UserModel $user):Response
{
$statusCode = Response::HTTP_OK;
if (!$user->id) {
$statusCode = Response::HTTP_NOT_FOUND;
}
return response(view('user.index', ['user' => $user]), $statusCode);
}
##まとめ
A・DRパターンを適用すると、MVCパターンよりもより処理の内容が具体化される。クラスが増えるのがデメリットでもあるが、整理された小さな機能の集まりで責務が明確化されているため、メリットでもある。
・重要なことはどのような考え方でクラスや処理グループを分割し、各クラスの処理を小さくすることで仕様変更やテストがようになるということ。
・開発者はそれぞれのあプリケーションに合わせて最適な設計パターンを採用しなければならない。