5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

BEAR.SundayAdvent Calendar 2020

Day 19

BEAR.Sundayの出力を探す

Last updated at Posted at 2020-12-18

Hello

こんにちは。 @yuki777 です。
これはBEAR.Sunday Advent Calendar 2020の19日目の記事です。
BEAR.Sundayの出力を追ってみました。

BEAR.Sundayの出力を探す

  • BEAR.SundayはPHPのフレームワークです。
  • PHPならばどこかでechoprintを実行して出力しているのではないでしょうか?
  • 出力している箇所を探してフレームワークへの理解を深めてみます。
  • grepなどを使わずに、エントリポイントから処理を順に追ってみます。
  • ソースはマニュアルのクイックスタートを使います。
composer create-project -n bear/skeleton MyVendor.MyProject
cd MyVendor.MyProject

エントリポイント

bin/page.php (CLI)

  • CLIの場合は php bin/page.php get / なのでbin/page.phpを見てみます。Bootstrapを実行しています。
exit((new Bootstrap())(PHP_SAPI === 'cli' ? 'cli-hal-app' : 'hal-app', $GLOBALS, $_SERVER));

public/index.php (Web)

  • Webの場合は composer servecomposer.jsonにより下記のとおりです。
"serve": "php -dzend_extension=xdebug.so -S 127.0.0.1:8080 -t public"
-t オプションを使えば、ドキュメントルートを明示的に指定することができます。 
URI リクエストにファイルが含まれない場合は、指定したディレクトリにある index.php あるいは index.html を返します。
  • というわけで public/index.php を見てみます。こちらもBootstrapを実行しています。
exit((new Bootstrap())(PHP_SAPI === 'cli-server' ? 'hal-app' : 'prod-hal-app', $GLOBALS, $_SERVER));

MyVendor\MyProject\Bootstrap

  • エントリポイントはCLIもWEBも Bootstrap が実行されていました。
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() メソッドは、 スクリプトがオブジェクトを関数としてコールしようとした際にコールされます。
  • $app->httpCache->transfer(); // *1 は名前の通りキャッシュをレスポンスしているようです。
  • $app->resource->{$request->method}->uri($request->path)($request->query); // *2 はリクエストされたPathやQueryを引数にリソースをリクエストしています。
  • assert($response instanceof ResourceObject); // *3により$responseResourceObjectです。
    • assertによると
      • assertionfalse であるかどうかを調べる
      • assert() は PHP 7 で言語構造となり、expectation の定義を満たすようになりました。
      • すなわち、開発環境やテスト環境では有効であるが、運用環境では除去されて、まったくコストのかからないアサーションということです
    • instanceof あるPHP変数が特定のクラスのオブジェクトのインスタンスであるかどうかを調べます
  • そして、$response->transfer($app->responder, $server); // *4 で、ResourceObjecttransferが実行されます。
  • 今度はResourceObjecttransferを見てみます。

BEAR\Resource\ResourceObject

  • エントリポイント から Bootstrap そして ResourceObjecttransferが実行されている。という流れでした。
// ... 省略 ...
    public function transfer(TransferInterface $responder, array $server)
    {
        $responder($this, $server);
    }
// ... 省略 ...
  • TransferInterfaceimplementsした$responderという実装クラスが関数としてコールされるので__invoke()が実行されるということです。
  • では$responderを見てみます。これはBootstrap$response->transfer($app->responder, $server); // *4として渡されていました。
  • もういちどBootstrapを見てみます。

MyVendor\MyProject\Bootstrap

// もういちど
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のドキュメントによると、
    • BEAR\Package\Injector::getInstance()ではコンテキストに応じたインジェクターが渡されます。
    • プロダクションでは従来のDIのスクリプトファイルを書き出すScriptInjector、開発用ではDIファイルを書き出さないRay\Di\Injectorが渡されます。
  • assert($app instanceof App); // *6により、$appMyVendor\MyProject\Module\Appです。つぎはAppを見てみます。

MyVendor\MyProject\Module\App

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が引数に指定されています。
    • コンストラクタによると、 コンストラクタメソッドを有するクラスは、新たにオブジェクトが 生成される度にこのメソッドをコールします。
  • $this->responder = $responder; // *8$responderがセットされています。
  • Appにはこれ以上の処理はないため、またまたBootstrapに戻りヒントを探してみます。

MyVendor\MyProject\Bootstrap

// 本日三回目
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

// ... 省略 ...
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); // *9factory()が実行されています。
  • $module = (new Module())($meta, $context, ''); // *10Moduleを見てみます。

BEAR\Package\Module

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 $classnewされています。
    • CLIでコンテキストがcli-hal-appの場合はAppModule,HalModule,CliModuleの順
    • WEBでコンテキストがhal-appの場合はAppModule,HalModuleの順
    • コンテキストにより後の束縛が優先されるというのはここですね。

MyVendor\MyProject\Module\AppModule

<?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クラスを継承して、configureメソッドをオーバーライドすることで束縛作成しています。
  • $this->install(new PackageModule()); // *13AppModuleが必要なモジュールとしてPackageModuleをインストールしています。
AppModule => PackageModule
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
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()); // *15SundayModuleが必要なモジュールとしてHttpResponderModuleをインストールしています。
AppModule => PackageModule => SundayModule => HttpResponderModule
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 で、リンク束縛しています。
    • リンク束縛 によると、 リンク束縛は最も基本の束縛です。インターフェイスとその実装クラスを束縛します。
AppModule => PackageModule => SundayModule => HttpResponderModule => HttpResponder
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では$responder($this, $server);のようにコールしていましたね。

BEAR\Package\Context\HalModule

  • HalModuleにはTransferInterfaceの束縛がありません。

BEAR\Package\Context\CliModule

  • CliModuleにはTransferInterfaceインターフェイスにCliResponderの束縛があります
  • cli-hal-appコンテキストであれば、CliResponderが優先されて束縛されるということですね。
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
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による出力を探してみました。
  • フレームワークの処理の流れを少し理解できました。

明日

  • 明日は日曜日です。お楽しみに!
5
1
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
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?