Symfonyをインストールすると、LaravelやCakePHPなどと同様に、Webサーバから実行されるファイルpublic/index.php
が自動生成されます。
例えば、Laravelのpublic/index.php
ですが、
<?php
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
require __DIR__.'/../vendor/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(Kernel::class);
$response = $kernel->handle(
$request = Request::capture()
)->send();
$kernel->terminate($request, $response);
となっており、『app.phpをrequireして、リクエスト処理してレスポンス作って処理終了』みたく、処理を追っていけます。
ところが、Symfonyは他のフレームワークと比べるとこの public/index.php
が、ちょっと変わっています。
Symfonyだと、
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
こうなってます。
『無名関数をreturn!?』
全然意味がわかりません。というか今まで意識してなかったけどこんな感じになってると知りませんでした。(Symfony 5.3からこうなった)
というわけで、何をしてるか処理を追ってみました。
謎のオートロードファイル、"autoload_runtime.php"
そもそもプログラムを実行して何も出力せず、ただ無名関数をreturnすると、出力結果はどうなるんでしょうか?
予想はできるものの、一応確認してみます。
<?php
return function () { return 'hoge'; };
$ php ./test.php
Process finished with exit code 0
ですよね。。
では、再びSymfonyのpublic/index.php
を見ていきましょう。よく見るとオートロードがちょっと違うのに気づきます。
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
autoload.php
ではなく、autoload_runtime.php
となっています。
ははーん、このautoload_runtime.php内で、わーって処理してるな
こう考えざるをえません。中身を見てみましょう。
<?php
// autoload_runtime.php @generated by Symfony Runtime
if (true === (require_once __DIR__.'/autoload.php') || empty($_SERVER['SCRIPT_FILENAME'])) {
return;
}
$app = require $_SERVER['SCRIPT_FILENAME'];
if (!is_object($app)) {
throw new TypeError(sprintf('Invalid return value: callable object expected, "%s" returned from "%s".', get_debug_type($app), $_SERVER['SCRIPT_FILENAME']));
}
$runtime = $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? 'Symfony\\Component\\Runtime\\SymfonyRuntime';
$runtime = new $runtime(($_SERVER['APP_RUNTIME_OPTIONS'] ?? $_ENV['APP_RUNTIME_OPTIONS'] ?? []) + [
'project_dir' => dirname(__DIR__, 1),
]);
[$app, $args] = $runtime
->getResolver($app)
->resolve();
$app = $app(...$args);
exit(
$runtime
->getRunner($app)
->run()
);
わーっと、処理が書いてあります。どうもSymfonyインストール時に生成されたようです。vendor/autoload.php
をrequireしてるので、必要なクラスのファイルはオートロードしてくれてそうです。やっぱりここでわーって処理してるな。。
しかし、最後の処理がとても気になります。
exit(
$runtime
->getRunner($app)
->run()
);
ちょっと待って、exit()
してる…
"autoload_runtime.php"を読み解く
ご存じの通り、プログラムは上から順に実行されます。public/index.php
に書いてる処理を上から順に箇条書きすると、
-
vendor/autoload_runtime.php
をrequireして実行する - 無名関数を返す
となります。
1が実行された後に2が実行されるわけですが、1で実行するvendor/autoload_runtime.php
は、最後の行でexit()
しています。
exit()
はそこでプログラムが終了するので、これ以降の処理である、2の処理は実行されないはずです。
なんだこれ。じゃ、なんで無名関数を返してるんだ?
一体何が起きてるのか、もっと詳しくvendor/autoload_runtime.php
を見ていく必要がありそうです。目を凝らして読んでいくと何やら怪しいコードがあります。
$app = require $_SERVER['SCRIPT_FILENAME'];
プログラムが実行された際に、$_SERVER['SCRIPT_FILENAME']
には、実行ファイルのパスが格納されています。
上で述べた通り、実行されるファイルはpublic/index.php
となるので、つまりこのコードは
$app = require 'public/index.php';
ということになります。
なんだこれ。無限ループじゃん
そう思ったものの、しかしながらSymfony内で無限ループしている気配は全く見えません。
※自分のコードが無限ループしている場合をのぞく
どういうことだよ…と思いながら、public/index.php
を見直してみると、あることに気づきます。
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
require
ではなくrequire_once
になっています。PHPの公式ドキュメントからの抜粋ですが、
require_once 式は require とほぼ同じ意味ですが、 ファイルがすでに読み込まれているかどうかを PHP がチェックするという点が異なります。 すでに読み込まれている場合はそのファイルを読み込みません。
require_once
は、一度ファイルを読み込んでいる場合は再度読み込むことはありません。再び処理を箇条書きにすると
-
require_onceで
vendor/autoload_runtime.php
を読み込んで実行 -
requireで
public/index.php
を読み込んで実行 -
require_onceなので、
vendor/autload_runtime.php
は読み込まずに処理続行 - 無名関数を返す
つまり、$app = require $_SERVER['SCRIPT_FILENAME'];
を処理した後の$appには
function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
}
public/index.php
の無名関数が格納されています。
"Symfony Runtime"を使った、アプリケーションの実行
なんかいいところまできている気がします。引き続きvendor/autoload_runtime.php
を見ていきましょう。
$runtime = $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? 'Symfony\\Component\\Runtime\\SymfonyRuntime';
$runtime = new $runtime(($_SERVER['APP_RUNTIME_OPTIONS'] ?? $_ENV['APP_RUNTIME_OPTIONS'] ?? []) + [
'project_dir' => dirname(__DIR__, 1),
]);
どうも、環境変数に値がなければSymfonyRuntime
というクラスのインスタンスを作っているようです。
Symfony Runtime…そういえばファイルのコメントにも書いてあります。Symfony Runtimeとはなんでしょうか?
The Runtime Component decouples the bootstrapping logic from any global state to make sure the application can run with runtimes like PHP-PM, ReactPHP, Swoole, etc. without any changes.
翻訳:
ランタイムコンポーネントは、起動ロジックをグローバルな状態から切り離し、PHP-PM、ReactPHP、Swooleなどのランタイムでアプリケーションを変更することなく実行できるようにするものです。
5.2以前では、public/index.php
に起動ロジック(リクエストを受け取りアプリケーションを実行して、レスポンスを返す)が書いてありましたが、それを別のクラスに切り離して、そのクラスを差し替えることで他のランタイムでもフレームワーク側の処理を変えずに実行できるようになっているようです。
Symfony Runtimeでは、Runtime
とRunner
がセットになっており、Runtime
で必要なRunner
を指定し、Runner
で起動ロジックを実行します。上記の処理では、SymfonyRuntime
のインスタンスを作成していますが、環境変数で独自のRutimeクラスを渡すとそのインスタンスが生成されます。
処理は続き
[$app, $args] = $runtime
->getResolver($app)
->resolve();
と、なっています。Resolver
を使ってresolveした結果を$app, $args
に入れていますが、ClosureResolver
というクラスを使い、$appにはpublic/index.php
の無名関数、$argsには環境変数が返ってきます。なお、開発時にはここでDebugClosureResolver
というクラスが使われるようになります。DebugClosureResolver
の場合は、無名関数の中身を解析して、問題がある場合はExceptionを返すようになっています。
$app = $app(...$args);
そして、$appに$app(...$args);
の結果が返されます。
これはつまり、
$app = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
ということだったのです!!
処理を追いかけた時の心の叫び: な、なんだってー!
ついに最後の処理です。
exit(
$runtime
->getRunner($app)
->run()
);
SymfonyRuntime::getRunner()
にKernel
インスタンスを渡し、Runner
を受け取ります。そしてrun()
しています。
public function getRunner(?object $application): RunnerInterface
{
if ($application instanceof HttpKernelInterface) {
return new HttpKernelRunner($application, Request::createFromGlobals());
}
... 以下略
となっています。起動ロジックはHttpKernelRunner
に入っていそうです。ついでにここでRequestオブジェクトも生成しているようです。
では、run()
部分を見ていきましょう。
class HttpKernelRunner implements RunnerInterface
{
private $kernel;
private $request;
public function __construct(HttpKernelInterface $kernel, Request $request)
{
$this->kernel = $kernel;
$this->request = $request;
}
public function run(): int
{
$response = $this->kernel->handle($this->request);
$response->send();
if ($this->kernel instanceof TerminableInterface) {
$this->kernel->terminate($this->request, $response);
}
return 0;
}
}
あー。いい感じで起動していますね。参考までにSymfony 5.2の頃のpublic/index.php
を見てみましょう。
<?php
use App\Kernel;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\HttpFoundation\Request;
require dirname(__DIR__).'/vendor/autoload.php';
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
if ($_SERVER['APP_DEBUG']) {
umask(0000);
Debug::enable();
}
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
下の部分がまったく同じですね。
Symfonyのpublic/index.phpはなぜ"無名関数"をreturnしているだけなのか
ここで、再び処理を箇条書きにして見直してみましょう。
-
require_onceで
vendor/autoload_runtime.php
を読み込んで実行 -
requireで
public/index.php
を読み込んで実行 -
require_onceなので、
vendor/autload_runtime.php
は読み込まずに処理続行 - 無名関数を返す
-
Symfony Runtimeのインスタンスを環境変数から判断して生成する(デフォルトは
SymfonyRuntime
) - Runtimeに無名関数を渡し、Resolverを介して、無名関数と環境変数パラメータを返す
- 無名関数を実行して、
Kernel
インスタンスを生成する。 -
Runtime::getRunner() に
Kernel
を渡してRunnerを受け取り、実行する - Runner内で起動ロジックを実行する
上記の手順を踏んで、Symfonyのアプリケーションが実行されていることがわかりました。
ということで、Symfonyのpublic/index.phpはなぜ"無名関数"をreturnしているだけなのかの答えは
Symfony RuntimeにKernelインスタンスを渡して、適切なRunnerを利用してアプリケーションを実行するため
でした。そして、なぜこのようなことをしているかというと
起動ロジックをRunnerに隠蔽し、Runnerを使ってアプリケーション起動する仕組みを使うことで、Symfony側のロジックを一切変更せず、SwooleやRoadrunnerなどでの実行を用意に切り替えれるようにするため
でした。
付録:現在公開されているRuntime
独自のRuntime/Runnerを作れば、Swooleなど使えるということがわかりましたが、実はすでにメジャーどころは用意されています。
ここではSymfonyのコアメンバーであるNyholmさんが作成したランタイムが公開されています。
現在、
- Swoole
- Roadrunner
- React PHP
- Bref
- Google Cloud
用のRuntimeが公開されています。使い方は超簡単。たとえばRoadrunnerだと
composer require spiral/roadrunner:v2.0 nyholm/psr7 # Roadrunnerインストール
composer require runtime/roadrunner-nyholm # Roadrunner用Runtimeインストール
APP_RUNTIME=Runtime\RoadRunnerNyholm\Runtime
vendor/bin/rr get-binary
./rr serve
おしまいです。