53
60

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Deep Dive into Laravel

Last updated at Posted at 2018-02-26

はじめに

本記事はLaravel(5.6)をより深く理解することを目的としており、アプリケーション開発のhow-toのような内容ではありませんので予めご了承ください。アプリケーション開発を行う上での情報は公式ドキュメントをご覧ください。

Laravel5.6公式ドキュメント
https://laravel.com/docs/5.6/

【追記 2018/03/25】
Eloquent編をアップしました。ご興味のある方はこちらもどうぞ。
Deep Dive into Laravel Eloquent ORM

Laravelのリクエスト ライフサイクル

まずは、リクエストを受けてレスポンスを返すまでの間にLaravel内でどのような処理が行われているのか見てみましょう。下図が全体の流れをシーケンス図風にまとめたものになります(スペースの関係上、細かい内容やメッセージ線を省略しているのでご了承ください)。

laravel.gif

上図で登場するオブジェクトは以下の4つです。

  • bootstrap ・・・起動スクリプト(bootstrap/app.php)
  • Container ・・・サービス コンテナ(Illuminate\Foundation\Application)
  • Component(framework) ・・・Laravelフレームワークの各種コンポーネント
  • Component(application) ・・・アプリケーションのコンポーネント(コントローラ etc)

それでは順番に処理を見て行きましょう。

:one: アプリケーションの作成

 
laravel.gif

クライアントからリクエストが送られてくると始めにアプリケーションの作成が行われます。ここでいうアプリケーションとはサービス コンテナ(DIコンテナ)を表します。アプリケーションの作成で行われる主な処理は以下の2つです。

  • サービス プロバイダの登録
  • コアクラスのエイリアスの登録

サービス プロバイダの登録では、以下の3つのサービス プロバイダがサービス コンテナに登録されます。

  • Illuminate\Log\LogServiceProvider
  • Illuminate\Events\EventServiceProvider
  • Illuminate\Routing\RoutingServiceProvider

そして、各サービス プロバイダからログやイベント、ルーティングに関する基本的なクラスがサービス コンテナに登録されます。

コアクラスのエイリアス登録では、以下のように一つのキーに対して複数のエイリアス名が登録される形になります。

Illuminate\Foundation\Application.php
[
	'app'                  => [\Illuminate\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class,  \Psr\Container\ContainerInterface::class],
	'auth'                 => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class],
	'auth.driver'          => [\Illuminate\Contracts\Auth\Guard::class],
	'blade.compiler'       => [\Illuminate\View\Compilers\BladeCompiler::class],
	'cache'                => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
	'cache.store'          => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class],
	'config'               => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class],
	'cookie'               => [\Illuminate\Cookie\CookieJar::class, \Illuminate\Contracts\Cookie\Factory::class, \Illuminate\Contracts\Cookie\QueueingFactory::class],
	'encrypter'            => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class],
	'db'                   => [\Illuminate\Database\DatabaseManager::class],
	'db.connection'        => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class],
	'events'               => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class],
	'files'                => [\Illuminate\Filesystem\Filesystem::class],
	'filesystem'           => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class],
	'filesystem.disk'      => [\Illuminate\Contracts\Filesystem\Filesystem::class],
	'filesystem.cloud'     => [\Illuminate\Contracts\Filesystem\Cloud::class],
	'hash'                 => [\Illuminate\Hashing\HashManager::class],
	'hash.driver'          => [\Illuminate\Contracts\Hashing\Hasher::class],
	'translator'           => [\Illuminate\Translation\Translator::class, \Illuminate\Contracts\Translation\Translator::class],
	'log'                  => [\Illuminate\Log\LogManager::class, \Psr\Log\LoggerInterface::class],
	'mailer'               => [\Illuminate\Mail\Mailer::class, \Illuminate\Contracts\Mail\Mailer::class, \Illuminate\Contracts\Mail\MailQueue::class],
	'auth.password'        => [\Illuminate\Auth\Passwords\PasswordBrokerManager::class, \Illuminate\Contracts\Auth\PasswordBrokerFactory::class],
	'auth.password.broker' => [\Illuminate\Auth\Passwords\PasswordBroker::class, \Illuminate\Contracts\Auth\PasswordBroker::class],
	'queue'                => [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class],
	'queue.connection'     => [\Illuminate\Contracts\Queue\Queue::class],
	'queue.failer'         => [\Illuminate\Queue\Failed\FailedJobProviderInterface::class],
	'redirect'             => [\Illuminate\Routing\Redirector::class],
	'redis'                => [\Illuminate\Redis\RedisManager::class, \Illuminate\Contracts\Redis\Factory::class],
	'request'              => [\Illuminate\Http\Request::class, \Symfony\Component\HttpFoundation\Request::class],
	'router'               => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class],
	'session'              => [\Illuminate\Session\SessionManager::class],
	'session.store'        => [\Illuminate\Session\Store::class, \Illuminate\Contracts\Session\Session::class],
	'url'                  => [\Illuminate\Routing\UrlGenerator::class, \Illuminate\Contracts\Routing\UrlGenerator::class],
	'validator'            => [\Illuminate\Validation\Factory::class, \Illuminate\Contracts\Validation\Factory::class],
	'view'                 => [\Illuminate\View\Factory::class, \Illuminate\Contracts\View\Factory::class],
]

補足説明するとサービス コンテナの内部では抽象エイリアスとエイリアスという2種類のエイリアスを持っていて、上記でいうと配列のキーが抽象エイリアスで配列の値(クラス名、インターフェース名)がエイリアスになります。

例えば、LogManagerはサービス プロバイダで以下のように抽象エイリアスを使ってサービス コンテナに登録されています。

Illuminate\Log\LogServiceProvider.php
$this->app->singleton('log', function () {
  return new LogManager($this->app);
});

コンポーネントを取得する場合は、抽象エイリアス / エイリアスのどちらを指定してもサービス コンテナが名前解決してくれて同じインスタンス(singletonの場合)を返してくれます。

var_dump(app('log') ===  app('Illuminate\Log\LogManager')); // boolean true
var_dump(app('log') === app('Psr\Log\LoggerInterface')); // boolean true

なお、混同しやすいのですが上記のエイリアスはconfig\app.phpで定義するFacadeのエイリアスとは別のものです。

config\app.php
'aliases' => [
  ...
  'Route' => Illuminate\Support\Facades\Route::class,
  ...

こちらのエイリアスはIlluminate\Foundation\AliasLoaderを使ってFacadeのインスタンスを取得するために使用されます。こちらも簡単に説明しておきますと、AliasLoaderはclass_alias関数を使用してエイリアスから実体となるクラスをオートロードしています。

例えば、ルートの定義でRoute::getと記述しますが、このRouteがエイリアスです。

Route::get('/', function () {
  return view('welcome');
});

RouteはAliasLoaderによってIlluminate\Support\Facades\Route::classと紐づけられているため、初回呼び出し時にIlluminate\Support\Facades\Routeがオートロードされます。Routeファサードクラスは、getFacadeAccessorrouterを返しますが、前述した抽象エイリアスになります。

Illuminate\Support\Facades\Route.php
protected static function getFacadeAccessor()
{
  return 'router';
}

Routingサービス プロバイダでIlluminate\Routing\Routerrouterとしてバインドしているため、ルートの定義のRoute::getIlluminate\Routing\Routerのgetメソッドを呼び出していることになります。

Illuminate\Routing\RoutingServiceProvider.php
$this->app->singleton('router', function ($app) {
  return new Router($app['events'], $app);
});

:two: bootstrapperによる設定

 
laravel.gif

アプリケーションの作成が終わるとbootstrapperが起動されます。ここでは、Illuminate\Foundation\Bootstrap直下にある以下のクラスのことを便宜的にbootstrapperと呼んでいます。

  • \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables
  • \Illuminate\Foundation\Bootstrap\LoadConfiguration
  • \Illuminate\Foundation\Bootstrap\HandleExceptions
  • \Illuminate\Foundation\Bootstrap\RegisterFacades
  • \Illuminate\Foundation\Bootstrap\RegisterProviders
  • \Illuminate\Foundation\Bootstrap\BootProviders

まず、最初に.envファイルが読み込まれます。環境変数を設定しておけば.env.localのように環境ごとにファイルを分けることができます。

例)Nginx(/etc/nginx/fastcgi_params)

fastcgi_param  APP_ENV  local;

なお、configのキャッシュを作成した場合、bootstrapperは.envファイルを読み込みません。

php artisan config:cache

.envファイルの読み込みが終わると次にconfigディレクトリ直下にあるapp.phpなどの各設定ファイルが読み込まれます(configのキャッシュがある場合はキャッシュを読み込む)。その次にbootstrapでサービス コンテナにバインドしている例外ハンドラがset_exception_handlerに設定されます。

bootstrap\app.php
$app->singleton(
  Illuminate\Contracts\Debug\ExceptionHandler::class,
  App\Exceptions\Handler::class
);

その次が前述したFacadeのエイリアスの登録になります。app.phpの”aliases”に設定した全エイリアスがここでAliasLoaderに登録されます。また、パッケージManifestファイル(後述)に設定されているエイリアスもここで登録されます。

そして、最後がサービス プロバイダの登録になります。app.phpに設定した”providers”に設定したプロバイダおよびパッケージManifestファイルに設定されているサービス プロバイダがサービス コンテナに登録されます。

パッケージManifestファイルとはbootstrap\cache\packages.phpファイルの事を指しています。このファイルはphp artisan package:discoverが実行された時に作成されるのですが、このコマンドが実行されるのが、composerのpost-autoload-dumpが実行されるタイミングになります。

composer.json
"scripts": {
  "post-root-package-install": [
    "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
  ],
  "post-create-project-cmd": [
    "@php artisan key:generate"
  ],
  "post-autoload-dump": [
    "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
    "@php artisan package:discover"
  ]
},

新しいパッケージが追加されるとartisanコマンドが起動されてvendor\composer\installed.jsonファイルが読み込まれ、extraプロパティに"laravel"が定義されているパッケージから"providers"と"aliases"を抽出してパッケージManifestファイルに出力します。

vendor\composer\installed.json
"extra": {
  "branch-alias": {
    "dev-master": "3.0-dev"
  },
  "laravel": {
    "providers": [
      "Laravel\\Socialite\\SocialiteServiceProvider"
    ],
    "aliases": {
      "Socialite": "Laravel\\Socialite\\Facades\\Socialite"
    }
  }
},

なお、app.phpとパッケージManifestファイルのプロバイダ情報は初回起動時にbootstrap\cache\services.phpというサービスManifestファイルに出力され、そちらがプロバイダの設定情報として読み込まれるようになります(新しいパッケージが追加・削除された場合は自動的に再作成される)。 1

サービスManifestファイルをもとにサービス プロバイダは"eager"と"deferred"に分類されてサービス コンテナに登録され、"deferred"で登録されたサービスプロバイダは必要になった時点でインスタンス化されサービス コンテナにバインドされます。

:three: リクエストの処理

 
laravel.gif

bootstrapperによる設定が終わるとリクエストがKernelに渡り、まず最初に以下の5つのグローバル ミドルウェアによってリクエストが処理されます。 2

  • \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode
  • \Illuminate\Foundation\Http\Middleware\ValidatePostSize
  • \App\Http\Middleware\TrimStrings
  • \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull
  • \App\Http\Middleware\TrustProxies

CheckForMaintenanceModeはアプリケーションがメンテナンスモードかどうかをチェックします(artisanコマンドを実行することでstorage\framework直下にdownという名前のファイルが作成されメンテナンスモードにすることができる)。メンテナンスモードの場合は503 HTTPステータスコードが返されます。

php artisan down

ValidatePostSizeはリクエストのCONTENT_LENGTHがphp.iniに設定されているpost_max_sizeを超えていないかどうかをチェックします。超えている場合は413 HTTPステータスコードが返されます。

TrimStringsはGETパラメータおよびPOSTパラメータの値(JSONリクエストの場合は各プロパティの値)をトリムします。ただし、"password"と"password_confirmation"という名前のパラメータはトリムしません。

ConvertEmptyStringsToNullはGETパラメータおよびPOSTパラメータの値(JSONリクエストの場合は各プロパティの値)が空文字の場合NULL値に変更します。

TrustProxiesはクライアントとWebサーバの間にロードバランサーやリバースプロキシを挟んでいる場合に、URLの生成やリダイレクトなどが正常に動作するようにリクエストにプロキシ情報を設定します。

App\Http\Middleware\TrustProxies.php
class TrustProxies extends Middleware
{
  // プロキシのアドレス
  protected $proxies = [
    '192.168.1.1',
    '192.168.1.2',
  ];

  // プロキシヘッダのマップ
  protected $headers = [
    Request::HEADER_FORWARDED => 'FORWARDED',
    Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
    Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
    Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
    Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
  ];
}

configにtrustedproxy.phpファイルを作成して設定することもできます。

config\trustedproxy.php
<?php

return [
  'proxies' => [
    '192.168.1.1',
    '192.168.1.2',
  ],
  
  'headers' => [
    \Illuminate\Http\Request::HEADER_FORWARDED => 'FORWARDED',
    \Illuminate\Http\Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
    \Illuminate\Http\Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
    \Illuminate\Http\Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
    \Illuminate\Http\Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
  ]
];

グローバル ミドルウェアの処理が終わるとリクエストにマッチするRoute情報を探し出し、設定されているコントローラ ミドルウェアにリクエストの処理をさせます。コントローラ ミドルウェアとは、コントローラのルートやコントローラのコンストラクタの中で指定したミドルウェアのことです。

// ルートで設定する例
Route::get('profile', 'UserController@show')->middleware('auth');
// コントローラのコンストラクタで設定する例
public function __construct()
{
  $this->middleware('auth');
}

ミドルウェアグループとミドルウェアはapp\Http\Kernel.phpに定義されています。

protected $middlewareGroups = [
  'web' => [
    \App\Http\Middleware\EncryptCookies::class,
    \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
    \Illuminate\Session\Middleware\StartSession::class,
    // \Illuminate\Session\Middleware\AuthenticateSession::class,
    \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    \App\Http\Middleware\VerifyCsrfToken::class,
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
  ],

  'api' => [
    'throttle:60,1',
    'bindings',
   ],
];

protected $routeMiddleware = [
  'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
  'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
  'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
  'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
  'can' => \Illuminate\Auth\Middleware\Authorize::class,
  'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
  'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];

app\Providers\RouteServiceProviderによってroutes/web.phpにはwebミドルウェアグループ、routes/api.phpにはapiミドルウェアグループが自動的に適用されます。

EncryptCookiesはCookieデータをopenssl_encryptおよびopenssl_decryptを使って暗号化・復号化します。暗号化・復号化で使用するキーは.envファイルに設定しているAPP_KEYになります。暗号化したくないcookieがある場合はミドルウェアの$exceptに名前を設定することで生データのまま保存することができます。

AddQueuedCookiesToResponseはcookieキューに保存されたcookieをレスポンスに追加します。レスポンスが作成される前に先にcookieに値をセットしたい場合にcookieキューを使用することができ、Cookieファサードを使用することでキューにcookieを追加することができます。

Cookie::queue('name', 'value', 60 * 24);

StartSessionconfig/session.phpが定義されている場合にセッションをスタートします。また、session.phpに設定したlotteryの値に基づきセッションのgcが行われます。

config/session.php
'lottery' => [2, 100], // random_int(1, 100) <= 2 がtrueの場合にgcが行われる

AuthenticateSession(デフォルトでコメントアウト)はセッションにユーザーのパスワードのハッシュ値を保存し、セッションに保存されたハッシュ値と現在のユーザーのパスワードのハッシュ値が一致しない場合は強制的にログアウトさせます。例えばパスワードが第3者に漏れて不正にログインされた場合、ユーザーがパスワードを変更すると第3者は強制的にログアウトされます。

ShareErrorsFromSessionはバリデーションエラーなどセッションに'errors'として保存されたデータをerrors変数としてビューにアサインします。

VerifyCsrfTokenはセッションに保存しているトークンとリクエストされたトークンが一致するかどうかをチェックします。トークンに一致しない場合は419 HTTPステータスコードを返します。トークンチェックをしたくないURLがある場合はミドルウェアの$exceptにURLを設定することでトークンチェックを回避することができます。メソッドが"HEAD"、"GET"、"OPTIONS"であったりコンソールからユニットテストを実行している場合はトークンチェックは行われません。

SubstituteBindingsはルートパラメータの値を別の値に付け替える処理を行います。具体的にはRoute::bindやモデル結合ルートがSubstituteBindingsの処理に該当します。

public function boot()
{
  parent::boot();

  // userパラメータの値からユーザー情報を取得し、userパラメータにモデルインスタンスをセットし直す
  Route::bind('user', function ($value) {
    return App\User::where('name', $value)->first() ?? abort(404);
  });
}
// userパラメータの値からユーザー情報を取得し、モデルインスタンスを自動的に注入
Route::get('api/users/{user}', function (App\User $user) {
  return $user->email;
});

Authenticateは指定されたガードでユーザー認証を行い、ユーザー認証に失敗したらログインページにリダイレクトします(JSONレスポンスの場合は401 HTTPステータスコード)。ガードを指定していない場合はconfig\auth.phpに設定しているデフォルトガードを使用します。なお、ガードは複数指定でき、どれか一つのガードで認証できれば認証成功になります。

AuthenticateWithBasicAuthはBasic認証によるログインを行います。Basic認証のユーザー名にはメールアドレスを使用します。認証に失敗した場合は401 HTTPステータスコードを返します。なお、AuthenticateWithBasicAuthはconfig\auth.phpに設定しているデフォルトガードを使用しますが、ドライバが"token"だとBasic認証は使用できません(TokenGuardにBasic認証処理が実装されていないため)。

SetCacheHeadersはレスポンスにキャッシュヘッダ(ETag、max-age、s-max-age)を設定します。

Route::get('/', function () {
  return view('welcome');
})->middleware('cache.headers:etag;max_age=120;s_maxage=60');

Authorizeは指定されたアビリティ(許可したい操作)およびルートパラメータ(もしくはモデルクラス名)を基にApp\Providers\AuthServiceProviderでマッピングされたポリシーを呼び出し認可処理を行います。権限がない場合は403 HTTPステータスコードを返します。ルートパラメータを指定した場合、前述したSubstituteBindingsによって暗黙のモデル結合が行われます。

// ルートパラメータにpostを指定
Route::put('/post/{post}', function (Post $post) {
  // do something
})->middleware('can:update,post');

RedirectIfAuthenticatedは指定されたガードによるログインチェックを行い、ユーザーがログイン済みの場合はホーム画面へリダイレクトします。ログインしていない場合はそのまま後続へ処理を引き渡します。

ThrottleRequestsはユーザーが同一ルートに対して一定の時間間隔(分)で何回までアクセス可能かを制限します。制限を超えた場合は429 HTTPステータスコードを返します。レスポンスヘッダーにX-RateLimit-LimitX-RateLimit-Remainingが付加され、制限値と残回数を確認することができます。また、制限を超えるとRetry-After(何秒後にリトライ可能)とX-RateLimit-Reset(制限がリセットされるUNIXタイムスタンプ)ヘッダーも付加されます。同一ユーザーの判別はログインユーザーの場合はユーザー情報のハッシュ値、ゲストユーザーの場合はサイトのドメインとIPアドレスを組み合わせたもののハッシュ値で行われます。なお、ユーザーの試行回数などのデータはconfig\cache.phpに設定されたドライバに基づきキャッシュとして保存されます。

// 1分間に5回まで
Route::get('/', function () {
  return view('welcome');
})->middleware('throttle:5,1');

上記、コントローラ ミドルウェアの処理が終わるとControllerDispatcherによりリクエストされたコントローラのアクションが実行され、その結果がレスポンスとしてクライアントに返されます。

以上がLaravelの大まかなリクエスト ライフサイクルになります。続いてLaravelのコアの部分であるサービス コンテナ、サービス プロバイダ、カーネル & ルーター、コントローラ、ビューを見て行きたいと思います(ここでは公式サイトの内容を補足していきます)。

サービスコンテナ

[公式サイト]Service Container
https://laravel.com/docs/5.6/container

LaravelのサービスコンテナはIlluminate\Foundation\Applicationです。サービスコンテナが実装しているインターフェースは以下のようなメソッドが定義されています(理解しやすいように順番を並び替えてグルーピングしています)。

Illuminate\Contracts\Container\Container.php
// コンテナにコンポーネントを登録する
public function bind($abstract, $concrete = null, $shared = false);
public function bindIf($abstract, $concrete = null, $shared = false);
public function singleton($abstract, $concrete = null);
public function instance($abstract, $instance);
public function extend($abstract, Closure $closure);
public function when($concrete);

// コンテナからコンポーネントを取得する
public function make($abstract, array $parameters = []);
public function factory($abstract);
public function call($callback, array $parameters = [], $defaultMethod = null);

// コンテナの状態を判定する
public function bound($abstract);
public function resolved($abstract);

// コンポーネントに情報を付加する
public function alias($abstract, $alias);
public function tag($abstracts, $tags);
public function tagged($tag);

// コンテナのイベントを監視する
public function resolving($abstract, Closure $callback = null);
public function afterResolving($abstract, Closure $callback = null);
Illuminate\Contracts\Foundation\Application.php
// アプリケーションの情報・状態など
public function version();
public function basePath();
public function environment();
public function isDownForMaintenance();
public function getCachedServicesPath();
public function getCachedPackagesPath();
public function runningInConsole();
public function runningUnitTests();

// サービス プロバイダの登録・起動
public function registerConfiguredProviders();
public function register($provider, $options = [], $force = false);
public function registerDeferredProvider($provider, $service = null);
public function boot();

// 起動イベントを監視する
public function booting($callback);
public function booted($callback);
Symfony\Component\HttpKernel\HttpKernelInterface.php
// リクエストの処理
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true);

インターフェースを見て分かるようにApplication(サービスコンテナ)の主な役割はコンポーネントの登録・管理、サービス プロバイダの登録・管理、リクエストの処理になります。

ただし、リクエストの処理に関しては、標準的なプロジェクト構成ではKernelが直接リクエストを処理するようになっています。

public\index.php
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
  $request = Illuminate\Http\Request::capture()
);

なお、サービス コンテナのbootメソッド(サービス プロバイダの起動)は、前述したリクエスト ライフサイクルのbootstrapperの最後で、BootProvidersというサービス プロバイダによって実行されます。

次にコンポーネントの登録・取得の内部動作を簡単に見てみましょう。

/* コンポーネントの登録例 */
$this->app->bind('App\Services\Twitter', function ($app) {
  return new App\Services\Twitter($app->make('App\Libraries\OAuthClient'));
});

// インターフェース名と実装クラス名を指定
$this->app->bind('App\Contracts\SocialMedia', 'App\Services\Twitter');
$this->app->bind(\App\Contracts\SocialMedia::class, \App\Services\Twitter::class);

/* コンポーネントの取得例 */
App::make('App\Services\Twitter'); // Appファサード利用
app('App\Services\Twitter'); // appヘルパー利用

コンポーネントを登録する場合は、上の例のように第1引数に実装クラス名やインターフェース名、第2引数にクロージャや実装クラス名を指定します。コンポーネントを取得する場合は、バインドする際に指定した実装クラス名やインターフェース名を指定します。

コンテナ内部ではbindメソッドで連想配列にバインド情報を保存し、makeメソッドでクロージャを実行して結果を返しています(singletonの場合は結果を保存)。第2引数に実装クラス名を指定した場合はリフレクションを使って実装クラスをインスタンス化します。実装クラスのコンストラクタに引数が指定されている場合も依存性を解決してインスタンス化してくれます。

// OAuthClientをインスタンス化して注入
class Twitter {
  public function __construct(OAuthClient $client) {}
}

// OAuthClientをインスタンス化して注入、$keyはデフォルト値が入る
class Twitter {
  public function __construct(OAuthClient $client, $key = 'xxxx') {}
}

// $keyが解決できないのでエラー
// 取得の際にkeyを指定すればOK app('App\Services\Twitter', ['key' => 'xxxxx']);
class Twitter {
  public function __construct(OAuthClient $client, $key) {}
}

なお、bindメソッドの第1引数にはインターフェース名(もしくは実装クラス名)を指定しますが、ユニークな文字列であればインターフェース名でなくても構いません。

$this->app->bind('socialmedia', 'App\Services\Twitter');

インスタンス結合も同様にクラス名である必要はないため、セッションやリクエストを使わずに値を引き継ぐような用途でも利用できます。

$this->app->instance('debug', true);

サービス プロバイダ

[公式サイト]Service Providers
https://laravel.com/docs/5.6/providers

サービス プロバイダは"eager"と"deferred"の2種類に分かれます。"eager"のサービス プロバイダはbootstrapperでコンテナに登録されるタイミングでregisterメソッドが実行され、全てのプロバイダの登録が完了したらすぐにbootメソッドが実行されます。

"deferred"のサービス プロバイダはコンテナからコンポーネントを取得するタイミングで、そのコンポーネントがまだコンテナに登録されていない場合にプロバイダがコンテナに登録されてregisterメソッドとbootメソッドが実行されます。

もう少し分かりやすいように実例でご説明します。
BroadcastServiceProviderはprovidesメソッドで3つのクラス名を返しています。

Illuminate\Broadcasting\BroadcastServiceProvider.php
public function provides()
{
  return [
    BroadcastManager::class,
    BroadcastingFactory::class,
    BroadcasterContract::class,
  ];
}

そして、前述したManifestファイルの作成のときに上記情報が参照されて以下のような内容のファイルが作られます。

bootstrap\cache\services.php
'deferred' => 
  array (
    'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
    'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
    'Illuminate\\Contracts\\Broadcasting\\Broadcaster' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',

この情報により3つのコンポーネントがBroadcastServiceProviderによって依存性を注入されるということをコンテナが把握します。そして、上記3つのうちどれか一つのコンポーネントをコンテナから取得しようとしたタイミングでBroadcastServiceProviderが呼ばれてregisterメソッドおよびbootメソッドが実行されます。一度プロバイダが実行されるとコンポーネントの依存性は解決されてコンテナに登録されるため、次回以降コンテナからコンポーネントを取得する際はプロバイダは実行されなくなります。

なお、フレームワーク内にあるサービス プロバイダの一覧を以下に列挙します。

サービス プロバイダ app.phpに定義あり 遅延 概要
Illuminate\Auth\AuthServiceProvider 認証・認可で必要となるコンポーネントを登録する
Illuminate\Auth\Passwords\PasswordResetServiceProvider パスワードリセットで必要となるコンポーネントを登録する
Illuminate\Broadcasting\BroadcastServiceProvider ブロードキャストで必要となるコンポーネントを登録する
Illuminate\Bus\BusServiceProvider 同期・非同期ジョブで必要となるコンポーネントを登録する
Illuminate\Cache\CacheServiceProvider キャッシュで必要となるコンポーネントを登録する
Illuminate\Cookie\CookieServiceProvider セッションで必要となるクッキー関連のコンポーネントを登録する
Illuminate\Database\DatabaseServiceProvider データベース接続で必要となるコンポーネントの登録を行う。また、プロバイダのboot時にEloquentモデルにConnectionResolverやEventDispatcherをセットする
Illuminate\Database\MigrationServiceProvider DBのマイグレーションで必要となるコンポーネントを登録する。このプロバイダはConsoleSupportServiceProvider内で登録される(configの記述不要)
Illuminate\Encryption\EncryptionServiceProvider 暗号化で必要となるコンポーネントを登録する
Illuminate\Events\EventServiceProvider イベントで必要となるコンポーネントを登録する。このプロバイダはフレームワーク内で自動的に登録される(configの記述不要)
Illuminate\Filesystem\FilesystemServiceProvider ファイルストレージで必要となるコンポーネントを登録する
Illuminate\Foundation\Providers\ArtisanServiceProvider Artisanコンソールの全コマンドを登録する。このプロバイダはConsoleSupportServiceProvider内で登録される(configの記述不要)
Illuminate\Foundation\Providers\ComposerServiceProvider フレームワーク内のComposer関連の処理で必要となるコンポーネントを登録する。このプロバイダはConsoleSupportServiceProvider内で登録される(configの記述不要)
Illuminate\Foundation\Providers\FormRequestServiceProvider プロバイダのboot時にコンテナイベント(フォームリクエストの初期処理、バリデーションエラー処理)を登録する。このプロバイダはFoundationServiceProvider内で登録される(configの記述不要)
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider MigrationServiceProvider、ArtisanServiceProvider、ArtisanServiceProviderを登録するためのアグリゲーションプロバイダ
Illuminate\Foundation\Providers\FoundationServiceProvider FormRequestServiceProviderの登録およびリクエストのvalidateマクロを設定する
Illuminate\Foundation\Support\Providers\AuthServiceProvider 認可のポリシーの登録を行う。アプリ側のAuthServiceProviderが継承する親クラス
Illuminate\Foundation\Support\Providers\EventServiceProvider イベントのlistenerとsubscriberを登録する。アプリ側のEventServiceProviderが継承する親クラス
Illuminate\Foundation\Support\Providers\RouteServiceProvider アプリケーションのルート情報を読み込む。アプリ側のRouteServiceProviderが継承する親クラス
Illuminate\Hashing\HashServiceProvider ハッシュで必要となるコンポーネントを登録する。デフォルトはCRYPT_BLOWFISH アルゴリズムだが、ドライバを指定することでArgon2 アルゴリズムも利用可能(php7.2以降)
Illuminate\Log\LogServiceProvider ログで必要となるコンポーネントを登録する。このプロバイダはフレームワーク内で自動的に登録される(configの記述不要)
Illuminate\Mail\MailServiceProvider メールで必要となるコンポーネントを登録する
Illuminate\Notifications\NotificationServiceProvider 通知で必要となるコンポーネントの登録およびboot時にビューに名前空間"notifications"を作成する
Illuminate\Pagination\PaginationServiceProvider ページネーションで必要となるコンポーネントの設定およびboot時にビューに名前空間"pagination"を作成する
Illuminate\Pipeline\PipelineServiceProvider パイプラインHubで必要となるコンポーネントを登録する
Illuminate\Queue\QueueServiceProvider 同期・非同期ジョブで必要となるコンポーネントを登録する
Illuminate\Redis\RedisServiceProvider Redisで必要となるコンポーネントを登録する
Illuminate\Routing\RoutingServiceProvider ルーティングで必要となるコンポーネントを登録する。このプロバイダはフレームワーク内で自動的に登録される(configの記述不要)
Illuminate\Session\SessionServiceProvider セッションで必要となるコンポーネントを登録する
Illuminate\Translation\TranslationServiceProvider 多言語化で必要となるコンポーネントを登録する
Illuminate\Validation\ValidationServiceProvider バリデーションで必要となるコンポーネントを登録する
Illuminate\View\ViewServiceProvider ビューで必要となるコンポーネントやビューエンジン(file、php、blade)を登録する

カーネル & ルーター

[公式サイト]Routing
https://laravel.com/docs/5.6/routing

カーネルはリクエストを受け取りルーターに渡すフロント コントローラの役割を果たします。カーネルが実装しているインターフェースは以下のようなメソッドが定義されています。

Illuminate\Contracts\Http\Kernel.php
public function bootstrap();
public function handle($request);
public function terminate($request, $response);
public function getApplication();

まず最初にbootstrapメソッドを実行してbootstrapperを起動し、bootstrapperによる処理が終わるとミドルウェアを介してルーターにリクエストの処理を引き継ぎます。コントローラでの処理が終わりミドルウェアを介してレスポンスをクライアントに送信した後にアプリケーションの終了処理を行います。もし、ミドルウェアにterminateメソッドが実装されている場合は、このタイミングでterminateメソッドが実行されます。

laravel.gif

次はルーターを見てみましょう。ルーター(Illuminate\Routing\Router)が実装しているインターフェースは以下のようなメソッドが定義されています。

Illuminate\Contracts\Routing\Registrar.php
// ルートの登録・設定
public function get($uri, $action);
public function post($uri, $action);
public function put($uri, $action);
public function delete($uri, $action);
public function patch($uri, $action);
public function options($uri, $action);
public function match($methods, $uri, $action);
public function resource($name, $controller, array $options = []);
public function group(array $attributes, $routes);

// ルートパラメータのバインド
public function substituteBindings($route);
public function substituteImplicitBindings($route);
Illuminate\Contracts\Routing\BindingRegistrar.php
// ルートパラメータのバインド
public function bind($key, $binder);
public function getBindingCallback($key);

インターフェースを見て分かるように、ルーターの基本的な役割はルートの登録・設定、ルートパラメータの作成になります。まず、アプリケーション側のApp\Providers\RouteServiceProviderによって指定されたルート定義ファイルroutes/web.phproutes/api.phpがルーターによってrequireされます。

App\Providers\RouteServiceProvider.php
Route::middleware('web')
  ->namespace($this->namespace)
  ->group(base_path('routes/web.php'));

Route::prefix('api')
  ->middleware('api')
  ->namespace($this->namespace)
  ->group(base_path('routes/api.php'));

ルート定義ファイルの中で以下のようにRouteファサードを使ってルート情報を登録していますので、ルート定義ファイルがrequireされたタイミングでルーターにルート情報が登録されます(ルート情報はIlluminate\Routing\Routeのコレクションで、それぞれのRouteが自分が担当するコントローラの実行を受け持つ)。

Route::get('foo', function () {
  return 'Hello World';
});

ルーターはリクエストを受け取るとリクエストメソッドとURIから該当するルートを見つけ、コントローラ ミドルウェアが定義されている場合はミドルウェアを介してルートに処理を引き継ぎます。

return (new Pipeline($this->container))
  ->send($request)
  ->through($middleware)
  ->then(function ($request) use ($route) {
    return $this->prepareResponse(
      $request, $route->run()
    );
  });

そしてルートがコントローラを実行して結果をルーターに返すと、ルーターは受け取ったデータの型から適切なレスポンスを作成します。

if ($response instanceof PsrResponseInterface) {
  $response = (new HttpFoundationFactory)->createResponse($response);
} elseif ($response instanceof Model && $response->wasRecentlyCreated) {
  $response = new JsonResponse($response, 201);
} elseif (! $response instanceof SymfonyResponse &&
  ($response instanceof Arrayable ||
  $response instanceof Jsonable ||
  $response instanceof ArrayObject ||
  $response instanceof JsonSerializable ||
  is_array($response))) {
  $response = new JsonResponse($response);
} elseif (! $response instanceof SymfonyResponse) {
  $response = new Response($response);
}

もし、該当するルートが見つからなかった場合は404 HTTPステータスコードを返します。なお、フォールバックルートを設定することでルートが見つからなかった場合の動作をカスタマイズすることができます。

routes\web.php
Route::fallback(function(){
  return response()->view('errors.notFound', [], 404);
});

以上がルーティングの一連の流れになります。

コントローラ

[公式サイト]Controllers
https://laravel.com/docs/5.6/controllers

コントローラの実行はルートが担当します。ルートはApp\Providers\RouteServiceProviderで指定した名前空間とルート定義ファイルで指定したコントローラ名@アクション名をもとに実行するコントローラとメソッドを決定します。

App\Providers\RouteServiceProvider.php
Route::middleware('web')
  ->namespace($this->namespace) // App\Http\Controllers
  ->group(base_path('routes/web.php'));
routes\web.php
Route::get('/home/', 'HomeController@index')->name('home');

もし、アクション名が指定されていない場合、クラスに__invokeメソッドが定義されているかどうかをチェックし、メソッドが定義されている場合はそれを実行メソッドに決定します。そしてサービス コンテナを介してコントローラをインスタンス化し(サービス コンテナにコントローラも登録される)、メソッドを実行します。

アプリケーションの基底コントローラ(App\Http\Controllers\Controller)は以下の3つのトレイトを使っています。

  • Illuminate\Foundation\Validation\ValidatesRequests
  • Illuminate\Foundation\Auth\Access\AuthorizesRequests
  • Illuminate\Foundation\Bus\DispatchesJobs

ValidatesRequestsはコントローラに検証メソッドを提供します。

public function store(Request $request)
{
    $this->validate($request, [
        'title' => 'required|max:255',
        'body' => 'required',
    ]);

}

これはリクエストオブジェクトのvalidateメソッドと同じ働きをします。

$request->validate([
  'title' => 'required|unique:posts|max:255',
  'body' => 'required',
]);

なお、Illuminate\Http\Request自体にはvalidateメソッドは定義されていません。validateメソッドはFoundationServiceProviderの中でmacroを使って後付けされています。

Illuminate\Foundation\Providers\FoundationServiceProvider.php
public function registerRequestValidate()
{
  Request::macro('validate', function (array $rules, ...$params) {
    validator()->validate($this->all(), $rules, ...$params);

    return $this->only(collect($rules)->keys()->map(function ($rule) {
      return str_contains($rule, '.') ? explode('.', $rule)[0] : $rule;
    })->unique()->toArray());
  });
}

AuthorizesRequestsはコントローラに認可メソッドを提供します。認可について公式サイトに記載がありますのでそちらをご確認ください。

[公式サイト]Authorization
https://laravel.com/docs/5.6/authorization

ただ、authorizeResourceメソッドの記載がないのでそこだけ簡単にご紹介します。これはリソースコントローラで認可処理を行う場合に利用できます。例えばコントローラのコンストラクタでメソッドを実行するとリソースコントローラのアクションに対して一括で認可処理を行うことができます。

public function __construct()
{
  $this->authorizeResource(Photo::class);
}

リソースコントローラのアクションとポリシーメソッドの対応は以下の通りです。

アクション ポリシーメソッド
show view
create create
store create
edit update
update update
destroy delete

DispatchesJobsはコントローラにジョブの実行メソッドを提供します。例えばartisanコマンドで同期ジョブを作成し、作成したジョブクラスのhandleメソッドに処理を実装します。

php artisan make:job SampleJob --sync
public function handle()
{
  // do something
}

そして、コントローラ側でdispatchメソッドを使ってジョブを実行します。

$this->dispatch(app('App\Jobs\SampleJob'));

ビジネスロジックなどをジョブとして切り出すことで再利用性を高め、コントローラの肥大化も防ぐことができます。

:bulb:ルートのキャッシュ

設定しているルートがコントローラベースのものだけであればルート設定をキャッシュすることにより、コントローラ実行までの時間を短縮することができます(クロージャベースのルートがある場合はキャッシュの作成不可)。

php artisan route:cache

キャッシュファイルはルート情報をシリアライズした形でbootstrap\cache\routes.phpに作られます。

app('router')->setRoutes(
    unserialize(base64_decode('TzozNDoiSWxsdW.....'))
);

キャッシュについて少しだけ補足説明すると、ルートはSymfonyのRouteCompilerによってコンパイルされてキャッシュされます。コンパイルを行うことで例えば以下のようなルートが正規表現に変換されます。

/password/reset -> #^/password/reset/(?P<token>[^/]++)$#su

Laravelではこの正規表現を使ってリクエストされたURLがマッチするかどうかを検証しています。ルート情報をキャッシュしていない場合は、ルート情報の登録に加えて、コンパイルしてルートが一致するかどうかの処理が毎回走るため、ルートの数が多ければ多いほどキャッシュの効果が大きくなります(ルートが100個あってリクエストURLが一番最後のルートに一致する場合、コンパイルが100回走ります)。

ビュー

[公式サイト]Views
https://laravel.com/docs/5.6/views

ビューの作成はviewヘルパーを使って行います。

Route::get('/', function () {
  return view('welcome', $data);
});

ビューには名前空間を付けることができます。例えばAppServiceProviderでViewファサードを使って以下のように名前空間を付けます。

View::addNamespace('pc', resource_path('views/pc'));
View::addNamespace('sp', resource_path('views/sp'));

コントローラ側で名前空間を指定することでビューを切り替えることができます。

return view('sp::welcome');

では、もう少しビューが作成されるまでの処理を詳しく見てみましょう。viewヘルパーの内部ではサービス コンテナからViewの作成を行うFactoryクラスを取得してViewインスタンスの作成を行っています。Factoryクラスは指定されたビュー名からファイルのパスを取得し、ファイルの拡張子でレンダリングエンジンを決定します。

LaravelにはFileエンジン、PHPエンジン、Compilerエンジンの3つのレンダリングエンジンがあり、Fileエンジンは拡張子が.css、PHPエンジンは拡張子が.php、Compilerエンジンは.blade.phpとなります。

PHPエンジンの場合はビューファイルがincludeされる際にviewヘルパーで渡したデータ配列がextract関数で展開されるためファイル内でデータを参照することができます。Fileエンジンの方はfile_get_contents関数でファイルを取得するだけなのでデータ配列を渡しても意味はありません。

FactoryクラスでViewインスタンスが作成されるとcreatingイベントがdispatchされます。Viewクリエイターを登録することで、このイベントをキャッチすることができます。例えば、ビューに結合したいデータがある場合、AppServiceProviderでViewクリエイターを登録すればViewインスタンスが作成される度にデータを結合することができます。

// クロージャではなくクラス名も指定可。クラスの場合はcreateメソッドを実装する
View::creator(['post', 'page'], function ($view) {
  $view->with('status', 1);
});

コントローラ側でwithメソッドを使って、Viewクリエイターで結合したデータを上書きすることもできます。

return view('welcome')->with('status', 2);

Viewインスタンスが作成され、コントローラからルーターにViewインスタンスが返されると、ルーターはViewインスタンスをもとにResponseオブジェクトを作成します。

Illuminate\Routing\Router.php
public static function toResponse($request, $response)
{
  ...

  } elseif (! $response instanceof SymfonyResponse) {
    $response = new Response($response);
  }

  ...
}

Viewクラスの__toStringでレンダリング処理が行われているため、ResponseオブジェクトにViewインスタンスを渡した時点でビューのレンダリングが始まります。

Illuminate\View\View.php
public function __toString()
{
  return $this->render();
}

まず、レンダリング開始直前にcomposingイベントがdispatchされます。Viewコンポーザーを登録することで、このイベントをキャッチすることができます。

// クロージャではなくクラス名も指定可。クラスの場合はcomposeメソッドを実装する
View::composer(['post', 'page'], function ($view) {
  $view->with('status', 1);
});

composingイベントの後、Viewインスタンスはレンダリングエンジンにビューファイルのパスとデータを渡します(Viewファサードのshareメソッドで共有されたデータもマージされる)。Compilerエンジンの場合、ビューファイルの最終更新日時とキャッシュファイル(storage\framework\views)の最終更新日時を比較し、ビューファイルが更新されていればコンパイル処理を開始します。

コンパイラはtoken_get_all関数でビューファイルをPHPトークンに分割し、トークンIDがT_INLINE_HTMLのコンテンツをIlluminate\View\Compilers\Concernsにある14個のコンパイルトレイトを使用してパースします。これを繰り返し全てのパースが終わって結果をマージし、ファイルをキャッシュフォルダに出力します(ファイル名はファイルパスをsha1でハッシュ化した値)。

キャッシュファイルはBladeの記法から普通のPHPの記法に変換されているので、最後にviewヘルパーで渡したデータ配列をextract関数で展開してキャッシュファイルに適用しレンダリングが終了となります。

以上がビュー作成の大まかな流れになります。

おまけ

Laravelのソースを眺めていてパイプラインの処理が面白かったので紹介します。

Laravelではミドルウェアの処理をパイプラインを使って実装しています。以下がミドルウェアを通してルーターにリクエストを渡している処理を抜粋したものです。

Illuminate\Foundation\Http\Kernel.php
protected function sendRequestThroughRouter($request)
{
  $this->app->instance('request', $request);
  Facade::clearResolvedInstance('request');
  $this->bootstrap();

  return (new Pipeline($this->app))
    ->send($request)
    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
    ->then($this->dispatchToRouter());
}

send()->through()->then()とメソッドチェーンで処理が繋がっていて、まさにパイプラインのような形になっています。では、Pipelineの実装を見てみましょう。

Illuminate\Pipeline\Pipeline.php
public function send($passable)
{
  $this->passable = $passable;

  return $this;
}

public function through($pipes)
{
  $this->pipes = is_array($pipes) ? $pipes : func_get_args();

  return $this;
}

public function then(Closure $destination)
{
  $pipeline = array_reduce(
    array_reverse($this->pipes), 
    $this->carry(),
    $this->prepareDestination($destination)
  );

  return $pipeline($this->passable);
}

sendメソッドでリクエストをpassableという変数に格納し、throughメソッドでミドルウェアの配列をpipesという変数に格納しています。そして、thenメソッドではarray_reduceを使って反転したミドルウェアの配列の各要素に対してcarry()というコールバック関数を適用し、返ってきたクロージャにリクエストを渡しています。prepareDestination()は処理の最初で使用されますので、以下の順番でコールバック関数が適用されることになります。

prepareDestination() -> middleware3 -> middleware2 -> middleware1

prepareDestinationの実装は以下のように、リクエストをdispatchToRouter()に受け渡すクロージャを返すようになっています。

Illuminate\Pipeline\Pipeline.php
protected function prepareDestination(Closure $destination)
{
  return function ($passable) use ($destination) {
    return $destination($passable);
  };
}

carryの実装は以下のようになっています。

Illuminate\Pipeline\Pipeline.php
protected function carry()
{
  return function ($stack, $pipe) {
    return function ($passable) use ($stack, $pipe) {
      if (is_callable($pipe)) {
        return $pipe($passable, $stack);
      } elseif (! is_object($pipe)) {
        list($name, $parameters) = $this->parsePipeString($pipe);
        $pipe = $this->getContainer()->make($name);
        $parameters = array_merge([$passable, $stack], $parameters);
      } else {
        $parameters = [$passable, $stack];
      }

      // $this->method = 'handle'
      return method_exists($pipe, $this->method)
        ? $pipe->{$this->method}(...$parameters)
        : $pipe(...$parameters);
    };
  };
}

$stackに前回の反復処理の結果、$pipeに現在の反復処理の値が入りますので、以下のような順番になります。

実行順 $stackに入る値 $pipeに入る値
prepareDestination()のリターン middleware3
①のリターン(クロージャ) middleware2
②のリターン(クロージャ) middleware1

従ってリクエストの処理は③⇒②⇒①の順番で行われることになります。まず、$pipeがmiddleware1になりますので1番目と2番目のif文はパスして$parameters = [$passable, $stack];でパラメータにリクエストと②のリターンが格納されて、middleware1のhandleメソッドが実行されます。ミドルウェアの実装が以下のようになりますので、前処理が入った後に$nextつまり②のリターンにリクエストが渡されます。

class SampleMiddleware
{
  public function handle($request, Closure $next)
  {
    /* 何か前処理が入る */

    $response = $next($request);
    
    /* 何か後処理が入る */

    return $response;
  }
}

そうすると今度は$stackに①のリターン、$pipeにmiddleware2が入りますので、またmiddleware2のhandleメソッドが実行されます。middleware2の前処理が終わると①のリターンにリクエストが渡り$stackにprepareDestination()のリターン、$pipeにmiddleware3が入ります。また、middleware3のhandleメソッドが実行され、middleware3の前処理が終わるとprepareDestination()のリターンにリクエストが渡ります。prepareDestination()のリターンはルーターにリクエストを渡すクロージャなので、つまりコントローラにリクエスト渡り、レスポンスが返って来ます。その後は、middleware3の後処理、middleware2の後処理、middleware1の後処理が行われてクライアントにミドルウェアの処理を通したレスポンスが返されることになります。

middleware1 -> middleware2 -> middleware3 -> コントローラ -> middleware3 -> middleware2 -> middleware1

send()->through()->then()の流れが個人的に好きだったので取り上げてみました:sweat_smile:

まとめ

タイトルにDeep Diveと書きましたが、一連の流れを書きたかったのでそこまで深く書けなかったかもしれません。期待した方ゴメンナサイ:bow:

機会があれば個別の機能ごとにDeep Diveしてみたいと思います。皆さんも興味があればDeep Diveしてみてください。きっと今まで知らなかったLaravelの機能やPHPの関数、随所にちりばめられたテクニックの数々を発見できると思います。

(内容に間違いなどありましたらコメント・編集リクエストを頂けると幸いです)

  1. composer.jsonにextraプロパティに"providers"および"aliases"が設定されているパッケージをインストールした場合は、app.phpにサービス プロバイダの定義を記述しなくても自動的に読み込まれます(例えばLaravel Socialiteとか)。

  2. HTTPテストの場合、継承するTestCaseクラス内でミドルウェアが無効化されているためリクエストは処理されません(サービス コンテナに'middleware.disable'を設定することで無効化できます)。

53
60
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
53
60

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?