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による出力を探してみました。
- フレームワークの処理の流れを少し理解できました。
明日
- 明日は日曜日です。お楽しみに!