実行環境を切り替える、"Runtime"

[Symfony Component Advent Calendar 2022](https://qiita.com/advent-calendar/2022/symfony-component)の18日目の記事です。

## 最初に
[Symfony](https://symfony.com)はPHPのフレームワークのひとつです。しかし、公式サイトの説明文には

> Symfony is a set of PHP Components, a Web Application framework, a Philosophy, and a Community — all working together in harmony.
  （SymfonyはPHPコンポーネントのセットで、Webアプリケーションフレームワークで、哲学、そしてコミュニティです。それらがハーモニーを奏でながら動作しています。）

と書かれている通り、PHPコンポーネントのセットで、たくさんのコンポーネントを提供しており、それらを組み合わせてひとつのフレームワークとして動作しています。Symfonyのコンポーネントは、Symfony上だけで動作するのではなく、他のPHPフレームワークやアプリケーションでも動作している強力なものが揃っています。

今回はそれらの中から、役立ちそうなもの・お薦めしたいものを紹介していきたいと思います。

※記事内ではautoloadのインポートは省略します。


## 実行環境を切り替える、"Runtime"

[Runtim](https://symfony.com/doc/current/components/runtime.html)は、アプリケーションの実行環境を簡単に切り替えることができるコンポーネントです。Symfony以外でも利用できます。


### インストール

```shell
composer require symfony/runtime
```

### 何をするのか？

`Runtime`はすごくざっくり言うと、**『リクエストを受けて、レスポンスを返す処理をまとめたもの』**を用意しています。この場合のリクエストは、CLIでの実行リクエストも含みます。例えばHTTP通信であれば、前回の[HttpKernel](https://qiita.com/ippey_s/items/96dff1edfec36f0d8cb8)の、リクエスト生成からレスポンス出力までのロジックが記述されたRuntimeがあり、そのRuntimeを実行することで、前述の処理が毎回実行できるようになります。

### RoadRunnerをつかう

例えば、[Roadrunner](https://roadrunner.dev/)でSymfonyを動かしたい場合、`Runtime`を使わなければ以下のようなWorkerを作成します。

```php:public/index.php

use Spiral\RoadRunner;
use Nyholm\Psr7;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\TerminableInterface;

include "vendor/autoload.php";

$psrFactory = new Psr7\Factory\Psr17Factory();
$worker = RoadRunner\Worker::create();
$worker = new RoadRunner\Http\PSR7Worker($worker, $psrFactory, $psrFactory, $psrFactory);

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);

$httpFoundationFactory = new HttpFoundationFactory();
$httpMessageFactory = new PsrHttpFactory($psrFactory, $psrFactory, $psrFactory, $psrFactory);

while ($request = $worker->waitRequest()) {
    try {
        $sfRequest = $httpFoundationFactory->createRequest($request);
        $sfResponse = $kernel->handle($sfRequest);
        $worker->respond($httpMessageFactory->createResponse($sfResponse));

        if ($thiskernel instanceof TerminableInterface) {
            $this->terminate($sfRequest, $sfResponse);
        }
    } catch (\Throwable $e) {
        $worker->getWorker()->error((string) $e);
    }
}
```

Workerを用意したら、`.rr.yaml`に設定して`rr serve`を実行すれば終了です。

```yaml/.rr.yaml
server:
  command: "php public/index.php"

http:
  address: 0.0.0.0:8080
  pool:
    num_workers: 4
```

#### Runtimeを利用する

もちろん上記でも動作しますが、**Symfonyが用意した`public/index.php`に手を入れる必要があります**。つまり、Symfonyがアップグレードした際に、`public/index.php`に対して変更があった場合、その変更に合わせて再度手を入れる必要があります。

そこで、`Runtime`を使うわけですが、上記のRoadrunnerように変更した箇所を`Runtime`に入れることで、`public/index.php`を修正することなく、Roadrunnerで実行できるようになります。

`Runtime`は`Runner`と`Runtime`を実装します。
`Runner`には上記のRoadrunner用に変更した処理を、`Runtime`は、前述の`Runner`を条件に合えば返します。

##### Runner

まずは`Runner`ですが、上記の処理をほぼそのまま移します。オートワイヤリングできるので、オートワイヤリング可能なものは`__construct()`でDIします。

```php:src/Runtime/RoadrunnerRunner.php


namespace App\Runtime;

use Nyholm\Psr7;
use Spiral\RoadRunner;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\TerminableInterface;
use Symfony\Component\Runtime\RunnerInterface;

class RoadrunnerRunner implements RunnerInterface
{
    private $kernel;
    private $httpFoundationFactory;
    private $httpMessageFactory;
    private $psrFactory;

    public function __construct(HttpKernelInterface $kernel, ?HttpFoundationFactoryInterface $httpFoundationFactory = null, ?HttpMessageFactoryInterface $httpMessageFactory = null)
    {
        $this->kernel = $kernel;
        $this->psrFactory = new Psr7\Factory\Psr17Factory();
        $this->httpFoundationFactory = $httpFoundationFactory ?? new HttpFoundationFactory();
        $this->httpMessageFactory = $httpMessageFactory ?? new PsrHttpFactory($this->psrFactory, $this->psrFactory, $this->psrFactory, $this->psrFactory);
    }

    public function run(): int
    {
        $worker = RoadRunner\Worker::create();
        $worker = new RoadRunner\Http\PSR7Worker($worker, $this->psrFactory, $this->psrFactory, $this->psrFactory);

        while ($request = $worker->waitRequest()) {
            try {
                $sfRequest = $this->httpFoundationFactory->createRequest($request);
                $sfResponse = $this->kernel->handle($sfRequest);
                $worker->respond($this->httpMessageFactory->createResponse($sfResponse));

                if ($this->kernel instanceof TerminableInterface) {
                    $this->kernel->terminate($sfRequest, $sfResponse);
                }
            } catch (\Throwable $e) {
                $worker->getWorker()->error((string) $e);
            }
        }

        return 0;
    }
}
```

##### Runtime

Runtime側は`GenericRunner`の子クラスを作ります。

```php:src/Runtime/RoadrunnerRuntime.php

namespace App\Runtime;

use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Runtime\GenericRuntime;
use Symfony\Component\Runtime\RunnerInterface;

class RoadrunnerRuntime extends GenericRuntime
{
    public function getRunner(?object $application): RunnerInterface
    {
        if ($application instanceof HttpKernelInterface) {
            return new RoadrunnerRunner($application);
        }

        // HttpKernelではなかった場合は、デフォルトのRunnerを実行する
        return parent::getRunner($application);
    }
}
```

##### 設定

上記で作成したRuntimeを使うように設定します。なんと`.env`に書くだけで設定できます。

```.env
APP_RUNTIME=App\Runtime\RoadrunnerRuntime
```

あとは、`rr serve`すれば動作します。


### 用意されているサードパーティRuntime

実は、すでにいくつかのRuntimeは用意されています。

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

現在、

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

が公開されており、これらを使えば簡単に実行環境を切り替えることができます。

## まとめ

今回は、`Runtime`を紹介しました。現在のSymfonyではこの`Runtime`を使うように`public/index.php`が記述されています。
どのようになってるか調べた記事があるので、こちらもぜひご覧ください。

https://qiita.com/ippey_s/items/71600ba5aa9809215172