本記事ではLaravelのアプリケーションを理解し、より良い設計・アーキテクチャを構築できるように学習したことを簡潔にまとめています。
#目次
1.Laravelのアーキテクチャ
2.アプリケーションのアーキテクチャ
3.HTTPリクエストとレスポンス
4.データベース
5.認証と許可
6.イベントとキューによる処理の分離
7.コンソールアプリケーション
8.テスト
9.エラーハンドリングとログの活用
10.テスト駆動開発の実践
#2.アプリケーションのアーキテクチャ
##2.2 レイヤードアーキテクチャ
アプリケーションで最も複雑になるのはモデルであり、データベースの操作とビジネス要求の処理を受け持つ。
しかし、ビジネスロジックとデータベースは密接に結合しないようにする必要がある。
→コードの複雑化を防ぐ。
フレームワークとアーキテクチャ
LarvelにはMVCパターンを適用するアプリケーション開発を簡単にする、数々の機能が用意されているが、ビジネス要求を完全に実現するのは難しく、個別に的確な構造化や設計技法が必要となる。
例えば、コントローラクラスやミドルウェアクラスにEloquentモデルによる大量のデータベース処理が記述されていると、データベースのリファクタリングやNoSQL導入で大幅な改修が必要となる。これらの問題は適切な設計パターンを定めていないことが原因であり、規模に合わせて最適な設計パターンを導入することで、整合性や品質、保守性を大きく改善することが可能になる。
##アーキテクチャ設計のポイント
アプリケーション設計では機能要件(ビジネスロジック)と非機能要件(セキュリティやパフォーマンス、サーバ負荷など)の両方を意識し、ある程度の基準を満たさなければならない。そのため、ビジネスロジックと非機能要件に相当するコードを分離しなければ、基準を満たすことは困難になる。
→適切な設計に基づく抽象化と構造化が必要になるため、前述のMVCパターンやADRパターンをベースにビジネスロジックからデータベース操作の処理を分離した設計を行うことが重要である
##レイヤードアーキテクチャ
責務が分離されているMVCパターンを採用した開発プロジェクトでも、ビューに関する処理や直接的なデータベースの処理が特定のクラスに直接記述されることがある。
まずは、下記にその例を示す。
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\User;
use App\Purchase;
class UserController
{
public function index(string $id)
{
$user = User::find(intval($id));
$purchase = Purchase::findAllBy($user->id);
//データベースから取得した値を使った処理が続く
return view('user.index', ['user' => $user]);
}
}
上記のコードで示した通り、1つのクラスに多くのデータベース処理が含まれており、このような実装方法では新たにビジネスロジックや仕様が追加された場合に、処理の内容把握が難しくなってしまう。
→こうした状況に陥らないためにも、それぞれの関心事を分離し、分離した要素に対し実装を行うことで、個々の要素をシンプルに保てるように注意しなければならない。
→レイヤードアーキテクチャは複雑になりやすい実装をいくつかのレイヤに分割して設計する手法
###レイヤ化のための概念
レイヤードアーキテクチャはいくつかの概念を元に分割して設計することが一般的です。MVCパターンでのモデルまたはコントローラといったクラスの肥大化を防ぐためには、いくつかの層に分割し、個々のクラスが持つ役割を小さくしなければならない。
(ポイント)
1.分割したレイヤの役割を明確にする。
2.上位レイヤが下位レイヤを呼び出すことを徹底し、その逆を禁止することで、レイヤ間の依存関係を明確にする
3.レイヤ化をする目的(ビジネスロジックの複雑化を防ぎ、依存を排除し、抽象化することで仕様変更への対応、テストの容易さ、アプリケーション開発が楽になる)を忘れない。
###モデルとコントローラの分離
早速分離を行っていく。
例としてデータベース処理がモデルとして役割をになっている場合、ビジネスロジックとEloquentモデル、そしてコントローラが強く結合する状態となる。これを解消するためにビジネスロジックのクラスをサービスクラスとして分離する。これが***「モデルとコントローラの分離」***である。
下記に、サービスクラスに分離することで、コントローラからデータベースの直接的な処理を排除した例を示す。
<?php
declare(strict_types=1);
namespace App\Services;
use App\User;
use App\Purchase;
class UserPurchaseService
{
public function retriecePurchase(int $identifier):User
{
$user = User::find($identifier);
$user->purchase = Purchase::findAllBy($user->id);
//データベースから取得した値を使った処理など
return $user;
}
}
UserPurchaseServiceクラスのretrievePurchaseメソッドとして、データベース操作を伴うビジネスロジックを分割している。このようなサービスクラスは、Laravelのコントローラクラスでコンストラクタインジェクションを用いて利用できる。
下記にコントローラクラスからの利用例を示す。
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
class UserController
{
protected $service;
public function __construct(UserPurchaseService $service)
{
$this->service = $service;
}
public funtion index(string $id)
{
$result = $this->service->retrievePurchase(intval($id));
return view('user.index',['user' => $result]);
}
}
上記コード例ではコントローラに直接記述していた処理をサービスクラスへと変更することによって、データベース処理などを直接記述していた実装を、サービスクラスのメソッドに置き換えることで処理内容が明確になっている。
###サービスレイヤとデータベースの分離
コントローラからデータベース処理を排除することができたが、ビジネスロジックを解決するクラスはまだデータベースに依存している状態である。ビジネスロジックとデータベース操作は直接的には関係しないため、分離してく。「サービスレイヤとデータベースの分離」
データベースへの依存を解決するために、データベース操作を抽象化し直接的な操作から分離するリポジトリと呼ばれる層を取り入れる。
(ポイント)
1.インターフェースを定義し、実装することでデータベース変更にも柔軟に対応できるようにする
2.戻り値の型を意識する(Eloquentやクエリビルダの戻り値はlaravelコレクションクラスだがNosqlなどはコレクションクラスを介さない)
3.通常ははPHPで利用される一般的な方での返却が望ましい
下記に、ユーザー情報をPHPの配列で返却するインターフェースを用意し、インターフェースとインターフェースを実装したリポジトリクラスのコード例を示す。
<?php
declare(strict_types=1);
namespace App\Repository;
interface UserRepositoryInterface
{
public function find(int $id): array;
}
<?php
declare(strict_types=1);
namespace App\Repository;
use App\User;
class UserRepository implements UserRepositoryInterface
{
public function find(int $id): array
{
$user = User::find($id)->toArray();
// ユーザー情報の抽出などの処理
return $user;
}
}
サービスクラスに上記のリポジトリインターフェースを指定することで、サービスクラスからもデータベースの直接的な操作を排除できる。
下記にユーザー情報取得処理の実装例を示す。
<?php
declare(strict_types=1);
namespace App\Service;
use App\Repository\UserRepositoryInterface;
use App\User;
class UserPurchaseService
{
protected $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function retriecePurchase(int $identifier):User
{
// リポジトリを介したデータの取得
$user = $this->userRepository->find($identifer);
// データベースから取得した値を使った処理
return $user;
}
}
##最後に
レイヤ化は処理を薄くすることが目的でなく、ビジネスロジックを表現するサービスレイヤから様々な日機能要件などを可能な限り取り除き、影響範囲を小さくすることが最大の目的である。