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

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

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

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

```php:test.php
<?php
return function () { return 'hoge'; };
```

```shell
$ php ./test.php

Process finished with exit code 0
```

ですよね。。

では、再びSymfonyの`public/index.php`を見ていきましょう。よく見るとオートロードがちょっと違うのに気づきます。

```php
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
```

`autoload.php`ではなく、`autoload_runtime.php`となっています。

**ははーん、このautoload_runtime.php内で、わーって処理してるな**

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

```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してるので、必要なクラスのファイルはオートロードしてくれてそうです。やっぱりここでわーって処理してるな。。
しかし、最後の処理がとても気になります。

```php
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`を見ていく必要がありそうです。目を凝らして読んでいくと何やら怪しいコードがあります。

```php
$app = require $_SERVER['SCRIPT_FILENAME'];
```

プログラムが実行された際に、`$_SERVER['SCRIPT_FILENAME']`には、実行ファイルのパスが格納されています。
上で述べた通り、実行されるファイルは`public/index.php`となるので、つまりこのコードは

```php
$app = require 'public/index.php';
```

ということになります。

**なんだこれ。無限ループじゃん**

そう思ったものの、しかしながらSymfony内で無限ループしている気配は全く見えません。
※自分のコードが無限ループしている場合をのぞく

どういうことだよ…と思いながら、`public/index.php`を見直してみると、あることに気づきます。

```php
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
```

`require`ではなく`require_once`になっています。[PHPの公式ドキュメント](https://www.php.net/manual/ja/function.require-once.php)からの抜粋ですが、

> require_once 式は require とほぼ同じ意味ですが、 ファイルがすでに読み込まれているかどうかを PHP がチェックするという点が異なります。 すでに読み込まれている場合はそのファイルを読み込みません。

`require_once`は、一度ファイルを読み込んでいる場合は再度読み込むことはありません。再び処理を箇条書きにすると

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

つまり、`$app = require $_SERVER['SCRIPT_FILENAME'];`を処理した後の$appには

```php
function (array $context) {
    return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
}
```

`public/index.php`の無名関数が格納されています。

## "Symfony Runtime"を使った、アプリケーションの実行

なんかいいところまできている気がします。引き続き`vendor/autoload_runtime.php`を見ていきましょう。

```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とはなんでしょうか？

https://symfony.com/doc/current/components/runtime.html


> 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クラスを渡すとそのインスタンスが生成されます。

処理は続き

```php
[$app, $args] = $runtime
    ->getResolver($app)
    ->resolve();
```

と、なっています。`Resolver`を使ってresolveした結果を`$app, $args`に入れていますが、`ClosureResolver`というクラスを使い、$appには`public/index.php`の無名関数、$argsには環境変数が返ってきます。なお、開発時にはここで`DebugClosureResolver`というクラスが使われるようになります。`DebugClosureResolver`の場合は、無名関数の中身を解析して、問題がある場合はExceptionを返すようになっています。

```php
$app = $app(...$args);
```

そして、$appに`$app(...$args);`の結果が返されます。
これはつまり、

```php
$app = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
```

ということだったのです！！


処理を追いかけた時の心の叫び：　**な、なんだってー！**


ついに最後の処理です。

```php
exit(
    $runtime
        ->getRunner($app)
        ->run()
);
```

`SymfonyRuntime::getRunner()`に`Kernel`インスタンスを渡し、`Runner`を受け取ります。そして`run()`しています。


```php:SymfonyRuntime.php

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

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

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

```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_once**で`vendor/autoload_runtime.php`を読み込んで実行
2. **require**で`public/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など使えるということがわかりましたが、**実はすでにメジャーどころは用意されています**。

https://github.com/php-runtime/runtime

ここではSymfonyのコアメンバーである[Nyholm](https://github.com/Nyholm)さんが作成したランタイムが公開されています。
現在、

- Swoole
- Roadrunner
- React PHP
- Bref
- Google Cloud

用のRuntimeが公開されています。使い方は超簡単。たとえばRoadrunnerだと

```shell
composer require spiral/roadrunner:v2.0 nyholm/psr7 # Roadrunnerインストール
composer require runtime/roadrunner-nyholm # Roadrunner用Runtimeインストール
```

```text:.env
APP_RUNTIME=Runtime\RoadRunnerNyholm\Runtime
```

```shell
vendor/bin/rr get-binary
./rr serve
```

おしまいです。