本記事ではLaravelのアプリケーションを理解し、より良い設計・アーキテクチャを構築できるように学習したことを簡潔にまとめています。
目次
1.Laravelのアーキテクチャ
2.アプリケーションのアーキテクチャ
3.HTTPリクエストとレスポンス
4.データベース
5.認証と許可
6.イベントとキューによる処理の分離
7.コンソールアプリケーション
8.テスト
9.エラーハンドリングとログの活用
10.テスト駆動開発の実践
1.Laravelのアーキテクチャ
1-3. DIとサービスコンテナ
アプリケーションを構築する際、拡張可能な設計を行うことは必要不可欠であり、そのためにもクラス通しは疎結合でなくてはならない。
この記事では、クラス通しが疎結合かつ拡張性の高い設計方法を学んでいく。
下記に利用者に対してメールで通知を送信するコードの例を示す。
class UserService
{
public function sendNotification(string $to, string $message): void
{
$mailsender = new MailSender();
$mailsender->send($to, $message);
}
}
class MailSender
{
public function send(string $to, string $message):void
{
// メール送信処理
}
}
<コード解説>
上記の例ではUserServiceクラスはMailSenderクラスに依存しており、例えば、機能がメールだけでなくメッセージやプッシュ通知に置き換わる場合、このままだと似たようなコードを別で用意しなくてはならない。
この課題は、sendNotificationの引数としてMailSenderクラスのインスタンスを渡すように設計することで解消できる。
class UserService
{
public function sendNotification(MailSender $mailsender, string $to, string $message):void
{
$mailsender->send($to, $message);
}
}
上記のように変更することでsendNotificationメソッドはMailSenderクラスだけでなく、その継承クラスも利用可能となり、特定クラスとの依存関係を排除できる。
このようにクラスやメソッド内で利用する機能を外部から渡す設計パターンがDI(依存性の注入)である。
続けて、上記のコードを「メールを送る」「メッセージを送る」など具体的な処理ではなく、「(何らかの手段で)通知する」インターフェースとして、より抽象的な役割を持つインターフェースとして持たせることでより拡張性の高いコードにしていく。
class UserService
{
public function sendNotification(NotifierInterface $notifier, string $to, string $message):void
{
$notifier->send($to, $message);
}
}
interface NotifierInterface
{
public function send(string $to, string $message):void;
}
class MailSender implements NotifierInterface
{
public function send(string $to, string $message):void
{
// メール送信
}
}
class PushSender implements NotifierInterface
{
public function send(string $to, string $message):void
{
// プッシュ通知
}
}
上記のコードでは「通知する」役割を担うNotifierInterfaceインターフェースを定義したことで、MailSenderもPushSenderも呼び出した方のsendが実行されるようになり、クラス間の関係が疎結合となった。
このようにクラス間では具象クラスではなく、抽象クラスを依存させることで疎結合となるた依存する設計の際には検討する方が良い。
コンストラクタインジェクション
クラスのコンストラクタの引数でインスタンスを注入する方法。
下記にコンストラクタインジェクションの例を示す
class UserService
{
protected $notifier;
public function __construct(NotifierInterface $notifier)
{
$this->notifier = $notifier;
}
public function sendNotification(string $to, string $message):void
{
$this->notifier->send($to, $message);
}
}
$user = app()->make(UserService:class);
$user->sendNotification('to', 'message');
上記のコードでは、サービスコンテナがコンストラクタの引数を読み取り、その引数がクラス名やインターフェース名であればその解決を行い、取得したインスタンスをコンストラクタの引数に渡す。(※インターフェースや抽象クラスを引数で指定した場合、あらかじめインターフェース名や抽象クラス名を解決する処理をバインドする必要がある)
app()->bind(NotifierInterface::class, function() {
return new MailSender(;
});
UserServiceクラスのクラス名をサービスコンテナで解決すると、コンストラクタインジェクションで、上記のコード例で解決されたMailSendderクラスのインスタンスが注入される。
なお、バインドせずにコンストラクタインジェクションを行うとインターフェース名は解決ができず例外が発生する。
メソッドインジェクション
メソッドの引数で必要とするインスタンスを渡す方法。サービスクラスのcallメソッドで対象メソッドを実行すると、サービスコンテナで解決して取得したインスタンスをメソッドに渡す流れになっている。
下記にUserServiceクラスを変更してメソッドインジェクションをを使用する形にしたコード例を示す。
class UserService
{
public function sendNotification(NotifierInterface $notifier, string $to, string $message):void
{
$notifier->send($to, $message);
}
}
$service = app(UserService::class);
$to = 'address';
$message = 'message';
app()->call([$service, 'sendNotification'], ['to'=> $to, 'message'=> $message]); // ①
<コード解説>
サービスコンテナのcallメソッドでは、第一引数に実行するクラスを解決して得られたインスタンスを保存する変数とメソッド名を指定し、第2引数にはメソッドインジェクションで注入する値以外の引数を配列で指定する。
コンテキストに応じた解決
他のクラスを呼び出す元のクラスに応じて解決するクラスを分けた場合、whenメソッドを指定する。
下記にコード例を示す。
class UserService
{
protected $notifier;
public function __construct(NotifierInterface $notifier)
{
$this->notifier = $notifire;
}
}
class AdminService
{
protected $notifier;
public function __construct(NotifierInterface $notifier)
{
$this->notifier = $notifier;
}
}
app()->when(UserService::class)
->needs(NotifierInterface::class)
->give(PushSender::class);
app()->when(AdminService::class)
->needs(NotifierInterface::class)
->give(MailSender::class);
上記コード例を示す通り、whenメソッドでは引数に注入先のクラス名を指定する。
続けてneedsメソッドでタイプヒンティングを指定し、
さらにサービスコンテナで解決する文字列をgiveメソッドで指定する。
以上でUserServiceのコンストラクタではPushSenderクラスのインスタンスを、
AdminServiceのコンストラクタではMailSenderクラスのインスタンスをそれぞれ注入される。