BEAR.Sundayの出力を探す

# Hello
こんにちは。 @yuki777 です。
これは[BEAR.Sunday Advent Calendar 2020](https://qiita.com/advent-calendar/2020/bear-sunday)の19日目の記事です。
BEAR.Sundayの出力を追ってみました。

# BEAR.Sundayの出力を探す
- [BEAR.Sunday](https://github.com/bearsunday/BEAR.Sunday)はPHPのフレームワークです。
- PHPならばどこかで`echo`や`print`を実行して出力しているのではないでしょうか？
- 出力している箇所を探してフレームワークへの理解を深めてみます。
- `grep`などを使わずに、エントリポイントから処理を順に追ってみます。
- ソースは[マニュアルのクイックスタート](https://bearsunday.github.io/manuals/1.0/ja/quick-start.html)を使います。

```
composer create-project -n bear/skeleton MyVendor.MyProject
cd MyVendor.MyProject
```

# エントリポイント
### bin/page.php (CLI)
- CLIの場合は `php bin/page.php get /` なので[bin/page.php](https://github.com/bearsunday/BEAR.Skeleton/blob/1.x/bin/page.php#L8)を見てみます。`Bootstrap`を実行しています。

```php
exit((new Bootstrap())(PHP_SAPI === 'cli' ? 'cli-hal-app' : 'hal-app', $GLOBALS, $_SERVER));
```

### public/index.php (Web)
- Webの場合は `composer serve` が [composer.json](https://github.com/bearsunday/BEAR.Skeleton/blob/1.x/composer.json#L47)により下記のとおりです。

```json
"serve": "php -dzend_extension=xdebug.so -S 127.0.0.1:8080 -t public"
```
- `php -S アドレス:ポート`でPHPの[ビルトインウェブサーバー](https://www.php.net/manual/ja/features.commandline.webserver.php)
が起動して、 `-t public`により `public/index.php` を参照します。

```
-t オプションを使えば、ドキュメントルートを明示的に指定することができます。 
URI リクエストにファイルが含まれない場合は、指定したディレクトリにある index.php あるいは index.html を返します。
```

- というわけで [public/index.php](https://github.com/bearsunday/BEAR.Skeleton/blob/1.x/public/index.php#L8) を見てみます。こちらも`Bootstrap`を実行しています。

```php
exit((new Bootstrap())(PHP_SAPI === 'cli-server' ? 'hal-app' : 'prod-hal-app', $GLOBALS, $_SERVER));
```

# MyVendor\MyProject\Bootstrap
- エントリポイントはCLIもWEBも [Bootstrap](https://github.com/bearsunday/BEAR.Skeleton/blob/1.x/src/Bootstrap.php#L42) が実行されていました。

```php
final class Bootstrap
{
    public function __invoke(string $context, array $globals, array $server): int
    {
        $app = Injector::getInstance($context)->getInstance(AppInterface::class); // *5
        assert($app instanceof App);
        if ($app->httpCache->isNotModified($server)) {
            $app->httpCache->transfer(); // *1

            return 0;
        }

        $request = $app->router->match($globals, $server);
        try {
            $response = $app->resource->{$request->method}->uri($request->path)($request->query); // *2
            assert($response instanceof ResourceObject); // *3
            $response->transfer($app->responder, $server); // *4

            return 0;
        } catch (Throwable $e) {
            $app->throwableHandler->handle($e, $request)->transfer();

            return 1;
        }
    }
```
- エントリポイントから`__invoke()`がコールされます。
  - [__invoke()](https://www.php.net/manual/ja/language.oop5.magic.php#object.invoke) メソッドは、 スクリプトがオブジェクトを関数としてコールしようとした際にコールされます。
- `$app->httpCache->transfer(); // *1` は名前の通りキャッシュをレスポンスしているようです。
- `$app->resource->{$request->method}->uri($request->path)($request->query); // *2` はリクエストされたPathやQueryを引数に[リソースをリクエスト](https://bearsunday.github.io/manuals/1.0/ja/resource.html#%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88)しています。
- `assert($response instanceof ResourceObject); // *3`により`$response` は`ResourceObject`です。
  - [assert](https://www.php.net/manual/ja/function.assert.php)によると
     - `assertion` が `false` であるかどうかを調べる
     - `assert()` は PHP 7 で言語構造となり、`expectation` の定義を満たすようになりました。 
     - すなわち、開発環境やテスト環境では有効であるが、運用環境では除去されて、**まったくコストのかからない**アサーションということです
  - [instanceof](https://www.php.net/manual/ja/language.operators.type.php) あるPHP変数が特定のクラスのオブジェクトのインスタンスであるかどうかを調べます
- そして、`$response->transfer($app->responder, $server); // *4` で、`ResourceObject`の`transfer`が実行されます。
- 今度は`ResourceObject`の`transfer`を見てみます。

# BEAR\Resource\ResourceObject
- エントリポイント から `Bootstrap` そして [ResourceObject](https://github.com/bearsunday/BEAR.Resource/blob/1.x/src/ResourceObject.php#L272)  の`transfer`が実行されている。という流れでした。

```php
// ... 省略 ...
    public function transfer(TransferInterface $responder, array $server)
    {
        $responder($this, $server);
    }
// ... 省略 ...
```

- `TransferInterface`を[implements](https://www.php.net/manual/ja/language.oop5.interfaces.php)した`$responder`という実装クラスが関数としてコールされるので__invoke()が実行されるということです。
- では`$responder`を見てみます。これは`Bootstrap`で  `$response->transfer($app->responder, $server); // *4`として渡されていました。
- もういちど`Bootstrap`を見てみます。

### MyVendor\MyProject\Bootstrap
```php
// もういちど
final class Bootstrap
{
    public function __invoke(string $context, array $globals, array $server): int
    {
        $app = Injector::getInstance($context)->getInstance(AppInterface::class); // *5
        assert($app instanceof App); // *6
        if ($app->httpCache->isNotModified($server)) {
            $app->httpCache->transfer(); // *1

            return 0;
        }

        $request = $app->router->match($globals, $server);
        try {
            $response = $app->resource->{$request->method}->uri($request->path)($request->query); // *2
            assert($response instanceof ResourceObject); // *3
            $response->transfer($app->responder, $server); // *4

            return 0;
        } catch (Throwable $e) {
            $app->throwableHandler->handle($e, $request)->transfer();

            return 1;
        }
    }
```

- `$app = Injector::getInstance($context)->getInstance(AppInterface::class); // *5` で`$app`がセットされています。
- [Injectorのドキュメント](https://bearsunday.github.io/manuals/1.0/ja/upgrade/injector.html)によると、
  - `BEAR\Package\Injector::getInstance()`ではコンテキストに応じたインジェクターが渡されます。 
  - プロダクションでは従来のDIのスクリプトファイルを書き出す`ScriptInjector`、開発用ではDIファイルを書き出さない`Ray\Di\Injector`が渡されます。
- `assert($app instanceof App); // *6`により、`$app`は`MyVendor\MyProject\Module\App`です。つぎは`App`を見てみます。


# MyVendor\MyProject\Module\App
- `$app->responder`を調べるために [MyVendor\MyProject\Module\App](https://github.com/bearsunday/BEAR.Skeleton/blob/1.x/src/Module/App.php) を見てみます。

```php
namespace MyVendor\MyProject\Module;

final class App implements AppInterface
{
    public HttpCacheInterface $httpCache;
    public RouterInterface $router;
    public TransferInterface $responder;
    public ResourceInterface $resource;
    public ThrowableHandlerInterface $throwableHandler;

    public function __construct(
        HttpCacheInterface $httpCache,
        RouterInterface $router,
        TransferInterface $responder, // *7
        ResourceInterface $resource,
        ThrowableHandlerInterface $throwableHandler
    ) {
        $this->httpCache = $httpCache;
        $this->router = $router;
        $this->responder = $responder; // *8
        $this->resource = $resource;
        $this->throwableHandler = $throwableHandler;
    }
}
```
- `__construct()`で `TransferInterface $responder, // *7`が引数に指定されています。
  - [コンストラクタ](https://www.php.net/manual/ja/language.oop5.decon.php)によると、 コンストラクタメソッドを有するクラスは、新たにオブジェクトが 生成される度にこのメソッドをコールします。
- `$this->responder = $responder; // *8`で`$responder`がセットされています。
- `App`にはこれ以上の処理はないため、またまた`Bootstrap`に戻りヒントを探してみます。

### MyVendor\MyProject\Bootstrap
```php
// 本日三回目
final class Bootstrap
{
    public function __invoke(string $context, array $globals, array $server): int
    {
        $app = Injector::getInstance($context)->getInstance(AppInterface::class); // *5
        assert($app instanceof App); // *6
        if ($app->httpCache->isNotModified($server)) {
            $app->httpCache->transfer(); // *1

            return 0;
        }

        $request = $app->router->match($globals, $server);
        try {
            $response = $app->resource->{$request->method}->uri($request->path)($request->query); // *2
            assert($response instanceof ResourceObject); // *3
            $response->transfer($app->responder, $server); // *4

            return 0;
        } catch (Throwable $e) {
            $app->throwableHandler->handle($e, $request)->transfer();

            return 1;
        }
    }
```
- `$app = Injector::getInstance($context)->getInstance(AppInterface::class); // *5`の処理は中を追っていませんでしたので、見てみます。

# BEAR\Package\Injector

- `Bootstrap`から[MyVendor\MyProject\Injector](https://github.com/bearsunday/BEAR.Skeleton/blob/1.x/src/Injector.php#L23) を経由してコールされる[BEAR\Package\Injector](https://github.com/bearsunday/BEAR.Package/blob/1.x/src/Injector.php)を見てみます

```php
// ... 省略 ...
final class Injector
{
    // ... 省略 ...
    public static function getInstance(string $appName, string $context, string $appDir, ?CacheProvider $cache = null): InjectorInterface
    {
        $injectorId = $appName . $context;
        if (isset(self::$instances[$injectorId])) {
            return self::$instances[$injectorId];
        }

        $meta = new Meta($appName, $context, $appDir);
        $cache = $cache ?? new PhpFileCache($meta->tmpDir . '/injector');
        $cache->setNamespace($injectorId);
        /** @var ?InjectorInterface $cachedInjector */
        $cachedInjector = $cache->fetch(InjectorInterface::class);
        if ($cachedInjector instanceof InjectorInterface) {
            return $cachedInjector;
        }

        $injector = self::factory($meta, $context); // *9
        $injector->getInstance(AppInterface::class);
        if ($injector instanceof ScriptInjector) {
            $cache->save(InjectorInterface::class, $injector);
        }

        self::$instances[$injectorId] = $injector;

        return $injector;
    }

    private static function factory(Meta $meta, string $context): InjectorInterface
    {
        $scriptDir = $meta->tmpDir . '/di';
        ! is_dir($scriptDir) && ! @mkdir($scriptDir) && ! is_dir($scriptDir);
        $module = (new Module())($meta, $context, ''); // *10
        $rayInjector = new RayInjector($module, $scriptDir);
        $isProd = $rayInjector->getInstance('', Compile::class);
        assert(is_bool($isProd));
        if ($isProd) {
            return new ScriptInjector($scriptDir, static function () use ($scriptDir, $module) {
                return new ScriptinjectorModule($scriptDir, $module);
            });
        }

        return $rayInjector;
    }
}
```
- `new Meta...`、`new PhpFileCache...`、`$cache->setNamespace...`をひとつずつ処理を追っていきます。
- `$injector = self::factory($meta, $context); // *9` で`factory()`が実行されています。
- `$module = (new Module())($meta, $context, ''); // *10` の`Module`を見てみます。


# BEAR\Package\Module
- [BEAR\Package\Module](https://github.com/bearsunday/BEAR.Package/blob/1.x/src/Module.php#L52)を見てみます。

```php
namespace BEAR\Package;

// ... 省略 ...

class Module
{
    /**
     * Return module from $appMeta and $context
     */
    public function __invoke(AbstractAppMeta $appMeta, string $context, string $cacheNamespace = ''): AbstractModule
    {
        $contextsArray = array_reverse(explode('-', $context));
        $module = new AssistedModule();
        foreach ($contextsArray as $contextItem) {
            $module = $this->installContextModule($appMeta, $contextItem, $module); // *11
        }

        $module->override(new AppMetaModule($appMeta));
        $module->override(new CacheNamespaceModule($cacheNamespace));

        return $module;
    }

    private function installContextModule(AbstractAppMeta $appMeta, string $contextItem, AbstractModule $module): AbstractModule
    {
        $class = $appMeta->name . '\Module\\' . ucwords($contextItem) . 'Module';
        if (! class_exists($class)) {
            $class = 'BEAR\Package\Context\\' . ucwords($contextItem) . 'Module';
        }

        if (! is_a($class, AbstractModule::class, true)) {
            throw new InvalidContextException($contextItem);
        }

        /** @psalm-suppress UnsafeInstantiation */
        $module = is_subclass_of($class, AbstractAppModule::class) ? new $class($appMeta, $module) : new $class($module); // *12

        return $module;
    }
}
```

- `$this->installContextModule($appMeta, $contextItem, $module); // *11` で、`installContextModule()`を実行しています
- `new $class($appMeta, $module) : new $class($module); // *12` `$class`が`new`されています。
  - CLIでコンテキストが`cli-hal-app`の場合は`AppModule`,`HalModule`,`CliModule`の順
  - WEBでコンテキストが`hal-app`の場合は`AppModule`,`HalModule`の順
  - [コンテキスト](https://bearsunday.github.io/manuals/1.0/ja/application.html#%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88)により後の束縛が優先されるというのはここですね。


# MyVendor\MyProject\Module\AppModule
- [AppModule](https://github.com/bearsunday/BEAR.Skeleton/blob/1.x/src/Module/AppModule.php#L18)を見てみます。

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyProject\Module;

use BEAR\Dotenv\Dotenv;
use BEAR\Package\AbstractAppModule;
use BEAR\Package\PackageModule;

use function dirname;

class AppModule extends AbstractAppModule
{
    protected function configure(): void
    {
        (new Dotenv())->load(dirname(__DIR__, 2));
        $this->install(new PackageModule()); // *13
    }
}
```
- `AbstractModule`クラスを[継承](https://www.php.net/manual/ja/language.oop5.inheritance.php)して、`configure`メソッドをオーバーライドすることで[束縛作成](https://bearsunday.github.io/manuals/1.0/ja/di.html#%E6%9D%9F%E7%B8%9B%E4%BD%9C%E6%88%90)しています。
- `$this->install(new PackageModule()); // *13` で`AppModule`が必要なモジュールとして`PackageModule`をインストールしています。

##### AppModule => PackageModule
- [BEAR\Package\PackageModule](https://github.com/bearsunday/BEAR.Package/blob/1.x/src/PackageModule.php#L31)を見てみます。


```php
namespace BEAR\Package;

class PackageModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->install(new QueryRepositoryModule());
        $this->install(new WebRouterModule());
        $this->install(new VndErrorModule());
        $this->install(new PsrLoggerModule());
        $this->install(new StreamModule());
        $this->install(new CreatedResourceModule());
        $this->install(new DiCompileModule(false));
        $this->install(new SundayModule()); // *14
    }
}
```
- `new QueryRepositoryModule`,`new WebRouterModule`,,, ひとつずつ見ていきます。
- `$this->install(new SundayModule()); // *14`で、`PackageModule`が必要なモジュールとして`SundayModule`をインストールしています。

##### AppModule => PackageModule => SundayModule
- [BEAR\Sunday\Module\SundayModule](https://github.com/bearsunday/BEAR.Sunday/blob/1.x/src/Module/SundayModule.php#L27)を見てみます。

```php
namespace BEAR\Sunday\Module;

class SundayModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->install(new AppModule());
        $this->install(new HttpCacheModule());
        $this->install(new DoctrineCacheModule());
        $this->install(new DoctrineAnnotationModule());
        $this->install(new ResourceModule('BEAR\Sunday'));
        $this->install(new RouterModule());
        $this->install(new HttpResponderModule()); // *15
        $this->install(new ErrorModule());
    }
}
```
- `$this->install(new HttpResponderModule()); // *15`で`SundayModule`が必要なモジュールとして`HttpResponderModule`をインストールしています。

##### AppModule => PackageModule => SundayModule => HttpResponderModule
- [BEAR\Sunday\Provide\Transfer\HttpResponderModule](https://github.com/bearsunday/BEAR.Sunday/blob/1.x/src/Provide/Transfer/HttpResponderModule.php#L14)を見てみます。

```php
namespace BEAR\Sunday\Provide\Transfer;

use BEAR\Sunday\Extension\Transfer\TransferInterface;
use Ray\Di\AbstractModule;

class HttpResponderModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->bind(TransferInterface::class)->to(HttpResponder::class); // *16
        $this->bind(HeaderInterface::class)->to(Header::class);
        $this->bind(ConditionalResponseInterface::class)->to(ConditionalResponse::class);
    }
}
```
- `$this->bind(TransferInterface::class)->to(HttpResponder::class); // *16` で、リンク束縛しています。
  - [リンク束縛](https://bearsunday.github.io/manuals/1.0/ja/di.html#%E3%83%AA%E3%83%B3%E3%82%AF%E6%9D%9F%E7%B8%9B) によると、 リンク束縛は最も基本の束縛です。インターフェイスとその実装クラスを束縛します。

##### AppModule => PackageModule => SundayModule => HttpResponderModule => HttpResponder
- `TransferInterface`インターフェイスの実装クラスである[BEAR\Sunday\Provide\Transfer\HttpResponder](https://github.com/bearsunday/BEAR.Sunday/blob/1.x/src/Provide/Transfer/HttpResponder.php#L41)を見てみます。

```php
namespace BEAR\Sunday\Provide\Transfer;

class HttpResponder implements TransferInterface
{
    public function __invoke(ResourceObject $ro, array $server): void
    {
        /** @var array{HTTP_IF_NONE_MATCH?: string} $server */
        $isModifed = $this->condResponse->isModified($ro, $server);
        $output = $isModifed ? $this->getOutput($ro, $server) : $this->condResponse->getOutput($ro->headers);

        foreach ($output->headers as $label => $value) {
            // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFallbackGlobalName
            header("{$label}: {$value}", false);
        }

        // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFallbackGlobalName
        http_response_code($output->code);

        echo $output->view; // *17 !!!
    }
}
```
- `echo $output->view; // *17`に、`echo`がありました。WEBの場合はここで出力されているということですね。
- ここは`__invoke()`ですので、オブジェクトを関数としてコールしようとした際にコールされます。
  - [ResourceObject](https://github.com/bearsunday/BEAR.Resource/blob/1.x/src/ResourceObject.php#L272)では`$responder($this, $server);`のようにコールしていましたね。

# BEAR\Package\Context\HalModule
- `HalModule`には`TransferInterface`の束縛がありません。

# BEAR\Package\Context\CliModule
- [CliModule](https://github.com/bearsunday/BEAR.Package/blob/1.x/src/Context/CliModule.php)には`TransferInterface`インターフェイスに`CliResponder`の束縛があります
- `cli-hal-app`コンテキストであれば、`CliResponder`が優先されて束縛されるということですね。

```php
namespace BEAR\Package\Context;

class CliModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->rename(RouterInterface::class, 'original');
        $this->bind(RouterInterface::class)->to(CliRouter::class);
        $this->bind(TransferInterface::class)->to(CliResponder::class); // *18
        $this->bind(HttpCacheInterface::class)->to(CliHttpCache::class);
        $stdIn = tempnam(sys_get_temp_dir(), 'stdin-' . crc32(__FILE__));
        $this->bind()->annotatedWith(StdIn::class)->toInstance($stdIn);
    }
}
```

##### CliModule => CliResponder
- [BEAR\Package\Provide\Transfer\CliResponder](https://github.com/bearsunday/BEAR.Package/blob/1.x/src/Provide/Transfer/CliResponder.php)です。
- `echo $ob; // *19` に`echo`がありました。
- CLIの場合はここで出力されているということですね。

```php
namespace BEAR\Package\Provide\Transfer;

final class CliResponder implements TransferInterface
{
    public function __invoke(ResourceObject $ro, array $server): void
    {
        /** @var array{HTTP_IF_NONE_MATCH?: string} $server */
        $isModified = $this->condResponse->isModified($ro, $server);
        $output = $isModified ? $this->getOutput($ro, $server) : $this->condResponse->getOutput($ro->headers);

        $statusText = (new Code())->statusText[$ro->code] ?? '';
        $ob = $output->code . ' ' . $statusText . PHP_EOL;

        // header
        foreach ($output->headers as $label => $value) {
            $ob .= "{$label}: {$value}" . PHP_EOL;
        }

        // empty line
        $ob .= PHP_EOL;

        // body
        $ob .= (string) $output->view;

        echo $ob; // *19 !!!
    }
}
```

# まとめ
- エントリポイントから処理を順に追ってechoによる出力を探してみました。
- フレームワークの処理の流れを少し理解できました。

# 明日
- 明日は日曜日です。お楽しみに！