LoginSignup
10
3

More than 1 year has passed since last update.

Symfonyのpublic/index.phpはなぜ"無名関数"をreturnしているだけなのか

Last updated at Posted at 2022-07-28

Symfonyをインストールすると、LaravelやCakePHPなどと同様に、Webサーバから実行されるファイルpublic/index.phpが自動生成されます。
例えば、Laravelのpublic/index.phpですが、

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だと、

public/index.php
<?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すると、出力結果はどうなるんでしょうか?
予想はできるものの、一応確認してみます。

test.php
<?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内で、わーって処理してるな

こう考えざるをえません。中身を見てみましょう。

vendor/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に書いてる処理を上から順に箇条書きすると、

  1. vendor/autoload_runtime.phpをrequireして実行する
  2. 無名関数を返す

となります。
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は、一度ファイルを読み込んでいる場合は再度読み込むことはありません。再び処理を箇条書きにすると

  1. require_oncevendor/autoload_runtime.phpを読み込んで実行
  2. requirepublic/index.phpを読み込んで実行
  3. require_onceなので、vendor/autload_runtime.phpは読み込まずに処理続行
  4. 無名関数を返す

つまり、$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では、RuntimeRunnerがセットになっており、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()しています。

SymfonyRuntime.php

    public function getRunner(?object $application): RunnerInterface
    {
        if ($application instanceof HttpKernelInterface) {
            return new HttpKernelRunner($application, Request::createFromGlobals());
        }
... 以下略

となっています。起動ロジックはHttpKernelRunnerに入っていそうです。ついでにここでRequestオブジェクトも生成しているようです。
では、run()部分を見ていきましょう。

HttpKernelRunner.php
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を見てみましょう。

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しているだけなのか

ここで、再び処理を箇条書きにして見直してみましょう。

  1. require_oncevendor/autoload_runtime.phpを読み込んで実行
  2. requirepublic/index.phpを読み込んで実行
  3. require_onceなので、vendor/autload_runtime.phpは読み込まずに処理続行
  4. 無名関数を返す
  5. Symfony Runtimeのインスタンスを環境変数から判断して生成する(デフォルトはSymfonyRuntime
  6. Runtimeに無名関数を渡し、Resolverを介して、無名関数と環境変数パラメータを返す
  7. 無名関数を実行して、Kernelインスタンスを生成する。
  8. Runtime::getRunner()Kernelを渡してRunnerを受け取り、実行する
  9. 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インストール
.env
APP_RUNTIME=Runtime\RoadRunnerNyholm\Runtime
vendor/bin/rr get-binary
./rr serve

おしまいです。

10
3
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
10
3