Hello
こんにちは。 @yuki777 です。
これはBEAR.Sunday Advent Calendar 2020の19日目の記事です。
BEAR.Sundayの出力を追ってみました。
BEAR.Sundayの出力を探す
- BEAR.SundayはPHPのフレームワークです。
- PHPならばどこかで
echoやprintを実行して出力しているのではないでしょうか? - 出力している箇所を探してフレームワークへの理解を深めてみます。
-
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 serveが composer.jsonにより下記のとおりです。
"serve": "php -dzend_extension=xdebug.so -S 127.0.0.1:8080 -t public"
-
php -S アドレス:ポートでPHPのビルトインウェブサーバー
が起動して、-t publicによりpublic/index.phpを参照します。
-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により$responseはResourceObjectです。-
assertによると
-
assertionがfalseであるかどうかを調べる -
assert()は PHP 7 で言語構造となり、expectationの定義を満たすようになりました。 - すなわち、開発環境やテスト環境では有効であるが、運用環境では除去されて、まったくコストのかからないアサーションということです
-
- instanceof あるPHP変数が特定のクラスのオブジェクトのインスタンスであるかどうかを調べます
-
assertによると
- そして、
$response->transfer($app->responder, $server); // *4で、ResourceObjectのtransferが実行されます。 - 今度は
ResourceObjectのtransferを見てみます。
BEAR\Resource\ResourceObject
- エントリポイント から
Bootstrapそして ResourceObject のtransferが実行されている。という流れでした。
// ... 省略 ...
public function transfer(TransferInterface $responder, array $server)
{
$responder($this, $server);
}
// ... 省略 ...
-
TransferInterfaceをimplementsした$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により、$appはMyVendor\MyProject\Module\Appです。つぎはAppを見てみます。
MyVendor\MyProject\Module\App
-
$app->responderを調べるために 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
-
BootstrapからMyVendor\MyProject\Injector を経由してコールされる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); // *9でfactory()が実行されています。 -
$module = (new Module())($meta, $context, ''); // *10のModuleを見てみます。
BEAR\Package\Module
- 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$classがnewされています。- CLIでコンテキストが
cli-hal-appの場合はAppModule,HalModule,CliModuleの順 - WEBでコンテキストが
hal-appの場合はAppModule,HalModuleの順 - コンテキストにより後の束縛が優先されるというのはここですね。
- CLIでコンテキストが
MyVendor\MyProject\Module\AppModule
- 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()); // *13でAppModuleが必要なモジュールとしてPackageModuleをインストールしています。
AppModule => PackageModule
- BEAR\Package\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()); // *15でSundayModuleが必要なモジュールとして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
-
TransferInterfaceインターフェイスの実装クラスであるBEAR\Sunday\Provide\Transfer\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);のようにコールしていましたね。
-
ResourceObjectでは
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
- BEAR\Package\Provide\Transfer\CliResponderです。
-
echo $ob; // *19にechoがありました。 - CLIの場合はここで出力されているということですね。
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による出力を探してみました。
- フレームワークの処理の流れを少し理解できました。
明日
- 明日は日曜日です。お楽しみに!