サイトアイコン 上尾市のWEBプログラマーによるブログ

「PHPフレームワーク Laravel Webアプリケーション開発」の感想・備忘録2

「PHPフレームワーク Laravel Webアプリケーション開発」の感想・備忘録1の続き

ライフサイクル

アプリケーション実行の流れ

  1. エントリポイント(public/index.php)
    フレームワークのセットアップを行い、サービスコンテナ(Illuminate\Foundation\Applicationインスタンス)を取得し、サービスコンテナを使ってapp/HTTP/Kernelインスタンスを生成する。
    リクエスト内容を元にIlluminate\HTTP\Requestインスタンスを生成し、HTTPカーネルのhandleメソッドに引き渡す。
  2. HTTPカーネル(app/HTTP/Kernel.php)
    ミドルウェアの設定などを行い、ルータにRequestインスタンスを渡す。
  3. ルータ(Illuminate\Routing\Router)
    ルート定義を読み込み、実行すべきミドルウェアがあればそれを実行し、最後にコントローラのメソッドを実行する。
  4. ミドルウェア
    コントローラの処理の前後に任意の処理を実行する。
    主な用途は、リクエストやレスポンスの値の変更や暗号化、セッション実行、認証処理など。
  5. コントローラ
    処理を行い、Illuminate\HTTP\ResponseまたはIlluminate\HTTP\JsonResponseインスタンスを返す。
    このインスタンスは、リクエストとは逆の順番でミドルウェア⇒ルータ⇒HTTPカーネル⇒エントリポイントと戻され、エントリポイントで出力される。

HTTPカーネル

app/HTTP/Kernelにはミドルウェアの設定だけが記述されている。
実際の処理は親クラスIlluminate\Foundation\HTTP\Kernelのhandleメソッドに記述されている。

handleメソッドでsendRequestThroughRouterメソッドを実行している。
sendRequestThroughRouterメソッドでは、ルータにRequestを渡して実行している。
tryブロック内に記述されているため、アプリケーション内で発生した例外は全てここでキャッチされる。

ビジネスロジックはコントローラではなくサービスクラスの実装する

実際のアプリケーションでは、ビジネスロジックはコントローラではなく、サービスクラスに実装することが多い。
サービスクラスをあらかじめサービスコンテナにバインドしておけば、アクションメソッドの引数としてインスタンスを受け取ることができる。

public function index(Request $request, TaskService $service)
{
    $tasks = $service->findTasks($request->get('hoge'));
    return view('task.index', compact('tasks'));
}

サービスコンテナ

サービスコンテナとは

サービスコンテナはシステム全体で利用するインスタンスを管理している。

インスタンスをコンストラクタやメソッドの引数として外部から渡す「DIパターン」を利用する場合も、インスタンスの生成や注入をサービスコンテナが行う。

バインドと解決

バインドと解決はサービスコンテナ(Illuminate\Foundation\Application)のインスタンスのメソッドで実行する。
多くの機能は親クラスのIlluminate\Container\Containerクラスに定義されている。

サービスコンテナインスタンスの取得方法

3つある。通常はapp();を使うことが多い。

  1. ヘルパ関数app()
    $app = app();
  2. クラスメソッドgetInstance()
    $app = \Illuminate\Foundation\Application::getInstance();
  3. Appファサードから取得
    $app = \App:getInstance();

バインドと解決のサンプル

app()->bind('hoge', function (\Illuminate\Foundation\Application $app) {
    return new \Illuminate\Support\Carbon();
});
echo app()->make('hoge');
// 'hoge'は通常は::classを使って\Aaa\Bbb::classのようにする

バインド

バインドの処理はapp/Providers/にクラスを作成して定義する。
または、app/Providers/AppServiceProvider.phpに定義する。

いくつかメソッドがあるが通常はbindメソッドかsingletonメソッドを使う。
第1引数: 文字列(名前)
第2引数: クロージャ(インスタンス生成処理)

app()->singleton('hoge', function (\Illuminate\Foundation\Application $app) {
    return new \Illuminate\Support\Carbon();
});
echo spl_object_id(app()->make('hoge'));
echo spl_object_id(app()->make('hoge')); // 同じIDになる

その他のメソッド

解決

サービスコンテナ(Illuminate\Foundation\Application)のインスタンスメソッドmake()で実行する。
app()->make(Hoge::class);

ヘルパ関数app()の引数に文字列を渡しても解決したインスタンスを取得することができるが、使うことはあまりないと思う。
app(Hoge::class);

バインドされていない文字列であっても、文字列がインスタンス化できるクラス(具象クラス)であれば、サービスコンテナがコンストラクタを実行してインスタンスを生成する。ただし、使うことはあまりないと思う。

class Hoge {
    public function echo() {
        echo 100;
    }
}
// というクラスに対して、app()->make(Hoge::class)->echo();

DIパターン

class UserService
{
    public function notice($to, $msg) {
        $mailsender = new MailSender();
        $mailsender->send(to, $msg);
    }
}

上記のコードでは、noticeメソッド内でMailSenderインスタンスを生成しているため、UserServiceクラスはMailSenderクラスに依存してしまっている。
例えば、MailSenderではなくPushSenderなど違う送信方法に変更する場合、コードの変更が必要となる。

noticeメソッドの仮引数でインスタンスを受けとることで、MailSenderだけでなくその継承クラスも利用可能となり、特定クラスとの依存関係を排除できる。
仮引数としてクラスではなくインターフェースを指定することで、メソッドが実装されていることが保証されるため、通常はインターフェースを指定する。

class UserService
{
    public function notice(SenderInterface $sender, $to, $msg) {
        $sender->send(to, $msg);
    }
}

コンストラクタインジェクション

Laravelではコンストラクタインジェクションにより、仮引数にタイプヒンティングを指定することで、コンストラクタの引数にインスタンスが注入される。

public function __construct(Hoge $hoge) {
    $this->hoge = $hoge;
}

タイプヒンティングにインターフェースや抽象クラスを指定する場合は、あらかじめサービスコンテナにバインドしておく必要がある。

class Hoge2
{
    public function __construct(HogeInterface $hoge) {
        $this->hoge = $hoge;
    }
}
// の場合
app()->singleton(HogeInterface::class, function (\Illuminate\Foundation\Application $app) {
    return new Hoge();
});
app()->make(Hoge2::class);

メソッドインジェクション

コントローラのアクションメソッドはメソッドインジェクションにより、仮引数にタイプヒンティングを指定することで、メソッドの引数にインスタンスが注入される。

public function index(Request $request, TaskService $service)
{
    $tasks = $service->findTasks($request->get('hoge'));
    return view('task.index', compact('tasks'));
}

コンストラクタインジェクションと同様に、タイプヒンティングにインターフェースや抽象クラスを指定する場合は、あらかじめサービスコンテナにバインドしておく必要がある。

仮引数にタイプヒンティングでクラスを指定したメソッドをサービスコンテナのcallメソッドを使って呼び出すと、引数にインスタンスが注入される。
ただし、可読性が落ちるので、あまり使わないほうがいいと思う。

public function echoCarbon(\Illuminate\Support\Carbon $carbon) {
    echo $carbon;
}
public function echoBye(HogeInterface $hoge) {
    echo $hoge->echo('bye');
}
// というメソッドがHoge2クラスにある場合、
$hoge2 = app()->make(Hoge2::class);
app()->call([$hoge2, 'echoCarbon']);
app()->call([$hoge2, 'echoBye']);

ファサードとは

クラスメソッド形式でフレームワークの機能を簡単に利用できるようにしたもの。
例えばConfigファサードを使ったコード\Config::get('app.debug');Configクラスにgetメソッドが実装されているように見えるが、実際にはConfigクラスは存在しない。

ファサードの裏側ではサービスコンテナが使われている。
どこからでも呼び出すことができるため煩雑なコードになる危険性があり、使うべきではないという意見もある。

ファサードの仕組み

Configファサードの実際のクラスは\Illuminate\Support\Facades\Configクラスであり、config/app.phpのaliasesでConfigというエイリアスが設定されている。
これによりuse Config; Config::xxx()または\Config::xxx()でアクセスできるようになっている。
(フレームワークのセットアップ時に、PHPのclass_alias関数が実行されている)
namespace指定がないファイルでは、use文なしでConfig::xxxだけでアクセス可能。
\Illuminate\Support\Facades\ConfigにはgetFacadeAccessorメソッドだけが定義されていて、getメソッドなどは定義されていない。
(親クラスの\Illuminate\Support\Facades\Facadeにも定義されていない)
\Illuminate\Support\Facades\Facadeにはマジックメソッド__callStaticが定義されていて、その中でサービスコンテナからインスタンスを取得してインスタンスメソッドを実行している。

__callStaticメソッドはstatic::getFacadeRoot()でサービスコンテナからインスタンスを取得している。
getFacadeRootメソッドは、return static::resolveFacadeInstance(static::getFacadeAccessor())となっていて、getFacadeAccessorメソッドで取得した名前をサービスコンテナの解決に使用している。
getFacadeAccessorメソッドはオーバーライドされることを想定していて、\Illuminate\Support\Facades\Configではreturn 'config';となっている。
ファサードで使うインスタンスのサービスコンテナへのバインドは、フレームワークのセットアップで行われている。
例えばconfigという名前のバインドは、/Illuminate/Foundation/Http/Kernel.phpのbootstrapメソッドで行われている。

ファサードの流れ

  1. \Config::get('app.debug');がコールされる。
  2. \Illuminate\Support\Facades\Configのgetメソッドを実行しようとする。
  3. getメソッドがないため、親クラス\Illuminate\Support\Facades\Facadeの__callStaticメソッドを実行する。
  4. __callStaticメソッドは、getFacadeRootメソッドでインスタンスを取得し、getメソッドを実行する。
    ※ getFacadeRootメソッドでは、getFacadeAccessorメソッドで取得した文字列をresolveFacadeInstanceメソッドによりサービスコンテナで解決している
モバイルバージョンを終了