この記事には @koriym さんによるフォローアップ記事: Re: BEAR.Sundayをコードリーディングしたのでメモ程度にアウトプットする - BEAR Blog があります。併せてご覧ください。
お久しぶりです。 @kalibora です。
題名が若干のルー大柴感ありますが、以前から気にはなっていたものの手を出していなかった BEAR.Sunday を最近触りまして、
理解を深めるためにもコードを多少読んだので、ここにそのメモを藪からスティックに垂れ流したいと思います。
誰かのお役に立てれば幸いです。
(多分に間違えている可能性があるので、その際はマサカリを投げてください。)
前提条件
以下のバージョンのコードを読みました。
$ composer show | grep 'bear/\(app-meta\|package\|resource\|sunday\)' bear/app-meta 1.2.4 BEAR.Sunday application meta information bear/package 1.7.0 BEAR.Sunday framework package bear/resource 1.9.0 Hypermedia framework for object as a service bear/sunday 1.2.1 A resource-oriented application framework
また、下記のように bear/skeleton
を使ってプロジェクトを開始しています。
$ composer create-project bear/skeleton MyVendor.MyPackage
読み進める前提としては、BEAR.Sunday の チュートリアル をやって、なんとなく理解していた方が分かりやすいかと思います。
3つのエントリポイント
何はともあれ開始地点であるエントリポイントを見てみます。 webからのアクセス用と、CLI用とで3つありました。
<?php // public/index.php $context = PHP_SAPI === 'cli-server' ? 'hal-app' : 'prod-hal-app'; require dirname(__DIR__) . '/bootstrap/bootstrap.php';
<?php // bootstrap/web.php $context = PHP_SAPI === 'cli' ? 'cli-hal-app' : 'hal-app'; require __DIR__ . '/bootstrap.php';
<?php // bootstrap/api.php $context = PHP_SAPI === 'cli' ? 'cli-hal-api-app' : 'hal-api-app'; require __DIR__ . '/bootstrap.php';
それぞれの違いは単純にコンテキストを変えているだけ。
そして bootstrap.php
を呼んでいるのみ。
bootstrap.php
では何をしているのか?
<?php // bootstrap/bootstrap.php use BEAR\Package\Bootstrap; use BEAR\Resource\ResourceObject; require dirname(__DIR__) . '/autoload.php'; /* @global string $context */ $app = (new Bootstrap)->getApp('MyVendor\MyPackage', $context, dirname(__DIR__)); $request = $app->router->match($GLOBALS, $_SERVER); try { $page = $app->resource->{$request->method}->uri($request->path)($request->query); /* @var $page ResourceObject */ $page->transfer($app->responder, $_SERVER); exit(0); } catch (\Exception $e) { $app->error->handle($e, $request)->transfer(); exit(1); }
このように非常に短いスクリプトで全体の流れを記述しているのみ。
次節からは $app
, $request
, $page
, $page->transfer()
, $app->error->handle()
について深掘りしていく。
$app
とは?
<?php $app = (new Bootstrap)->getApp('MyVendor\MyPackage', $context, dirname(__DIR__));
上記の $app
とは何者なのか?
<?php // `BEAR\Package\Bootstrap` より抜粋 use BEAR\Sunday\Extension\Application\AppInterface; final class Bootstrap { public function getApp(string $name, string $contexts, string $appDir = null) : AbstractApp { return $this->newApp(new AppMeta($name, $contexts, $appDir), $contexts); } public function newApp(AbstractAppMeta $appMeta, string $contexts, Cache $cache = null) : AbstractApp { // snip: $contexts が prod や stage だったらキャッシュから取得する処理 $app = (new AppInjector($appMeta->name, $contexts))->getInstance(AppInterface::class); // snip: キャッシュに保存する処理 return $app; } }
BEAR\Sunday\Extension\Application\AppInterface
をDI(Dependency Injection. 後述の AppInjector がその役割を担っている)で解決したインスタンスが $app
となる。
後述するが、結局の所これは MyVendor\MyPackage\Module\App
のインスタンスである。
AppInjector
?
AppInjector::getInstance()
メソッドでは、指定したinterfaceに束縛されたインスタンスを、依存解決済みで返してくれる。
(最初の1回目の場合は、依存解決したものをすべてフラットなPHPファイルとしてダンプする。これをコンパイル処理と呼んでいるみたい)
では、膨大なクラス同士の依存関係はどのように定義されているのか?
<?php // `BEAR\Package\AppInjector`から抜粋 use Ray\Di\AbstractModule; final class AppInjector implements InjectorInterface { /** * Return configured module */ private function newModule(AbstractAppMeta $appMeta, string $contexts) : AbstractModule { $contextsArray = array_reverse(explode('-', $contexts)); $module = null; foreach ($contextsArray as $context) { $class = $appMeta->name . '\Module\\' . ucwords($context) . 'Module'; if (! class_exists($class)) { $class = 'BEAR\Package\Context\\' . ucwords($context) . 'Module'; } if (! is_a($class, AbstractModule::class, true)) { throw new InvalidContextException($class); } /* @var $module AbstractModule */ $module = new $class($module); } $module->install(new ResourceObjectModule($appMeta)); $module->override(new AppMetaModule($appMeta)); return $module; } }
このように Ray\Di\AbstractModule
を継承したモジュールがその役割を担っている。
MyVendor\MyPackage\Module\{Context}Module
もしくは
BEAR\Package\Context\{Context}Module
が $contexts
の逆順で読み込まれる。
例えば、 $contexts = cli-hal-api-app
であれば、
MyVendor\MyPackage\Module\AppModule
BEAR\Package\Context\ApiModule
BEAR\Package\Context\HalModule
BEAR\Package\Context\CliModule
の順番で読み込まれる。しかし優先順位はその逆である。($module = new $class($module);
だと、引数で渡されたモジュールの方が優先度が低いので)
その後に ResourceObjectModule
を install し、 AppMetaModule
を
override しているので、最終的な優先順位は
BEAR\Package\AppMetaModule
BEAR\Package\Context\CliModule
BEAR\Package\Context\HalModule
BEAR\Package\Context\ApiModule
MyVendor\MyPackage\Module\AppModule
BEAR\Package\Provide\Resource\ResourceObjectModule
となる。
モジュールが依存関係(AOPの設定もしているけど)を定義していると述べたが、具体的にはどのように行っているのか?
AppMetaModule
を例に取ると、
<?php // `BEAR\Package\AppMetaModule`から抜粋 protected function configure() { // (snip) $this->bind(AppInterface::class)->to($this->appMeta->name . '\Module\App'); // (snip) }
このように AppInterface
を $this->appMeta->name . '\Module\App'
に束縛(bind
)している事がわかる。
この設定から、先に述べたように最終的に $app
は MyVendor\MyPackage\Module\App
のインスタンスとなるのだ!(ドヤァ)
$request
とは?
<?php $request = $app->router->match($GLOBALS, $_SERVER);
上記の $request
とは何者なのか?
前節で $app
は MyVendor\MyPackage\Module\App
のインスタンスであると述べた。
また、これは BEAR\Sunday\Extension\Application\AbstractApp
を継承している。
そしてデフォルトでは
AbstractApp::$router : RouterInterrface
は BEAR\Package\Provide\Router\WebRouter
に束縛されている。
この設定は下記のモジュールを見ると分かる。
BEAR\Package\PackageModule
BEAR\Package\Provide\Router\WebRouterModule
そういうわけで、実態である WebRouter
を見ていく。
// `BEAR\Package\Provide\Router\WebRouter` から抜粋 <?php public function match(array $globals, array $server) { $request = new RouterMatch; list($request->method, $request->query) = $this->httpMethodParams->get($server, $globals['_GET'], $globals['_POST']); $request->path = $this->schemeHost . parse_url($server['REQUEST_URI'], PHP_URL_PATH); return $request; }
BEAR\Package\Provide\Router\HttpMethodParams
も併せて読むと分かるが、
$_GET
や $_POST
や php://input
などの差異をうまく吸収して、
クエリ文字列だろうがPOSTされたボディであろうが、それらを気にせず、 $request->query
に入れてくれている。
また、 _method
や HTTP_X_HTTP_METHOD_OVERRIDE
を見てよしなに $request->method
に入れている。
例として、 コンテキストが api
で、 GET /path/to?key=value
をリクエストすると、
下記のような値が入る。
$request->method
:get
$request->query
:['key'=> 'value', 'hoge' => 'fuga']
$request->path
:app://self/path/to
HTTPリクエストから、BEARで扱う形式への変換、マッピングというのがこの処理の肝なのではないだろうか。
$page
とは?
<?php $page = $app->resource->{$request->method}->uri($request->path)($request->query);
上記の $page
とは何者なのか?
と、その前に分かりやすくするために、 GET /path/to?key=value
というリクエストが来たことにしておく。
するとこうなる。
<?php $page = $app->resource->get->uri('app://self/path/to')(['key'=> 'value', 'hoge' => 'fuga']);
デフォルトでは
AbstractApp::$resource : ResourceInterface
は BEAR\Resource\Resource
に束縛されている。
この設定は下記のモジュールを見ると分かる。
BEAR\Package\PackageModule
BEAR\Sunday\Module\SundayModule
BEAR\Sunday\Module\Resource\ResourceModule
BEAR\Resource\Module\ResourceClientModule
では実態である Resource
を見ていく。
<?php // `BEAR\Resource\Resource` から抜粋 // (snip) public function __get($name) { $this->method = $name; return $this; } // (snip) public function uri($uri) { if (is_string($uri)) { $uri = new Uri($uri); } $uri->method = $this->method; $resourceObject = $this->newInstance($uri); $resourceObject->uri = $uri; $this->request = new Request( $this->invoker, $resourceObject, $uri->method, $uri->query, [], $this->linker ); $this->method = 'get'; return $this->request; } // (snip) public function newInstance($uri) { return $this->factory->newInstance($uri); }
oops. まずは factory が何か知らないといけない。
詳細は割愛するが、 BEAR\Resource\Factory
がそれだ。
というわけでさらに BEAR\Resource\Factory
を読むと、
<?php // `BEAR\Resource\Factory` から抜粋 // (snip) /** * Resource adapter biding config * * @var SchemeCollectionInterface */ private $scheme; // (snip) public function newInstance($uri) { if (! $uri instanceof Uri) { $uri = new Uri($uri); } $adapter = $this->scheme->getAdapter($uri); return $adapter->get($uri); } // (snip)
というように、今度は SchemeCollectionInterface
にたどり着く。
これは BEAR\Resource\Module\SchemeCollectionProvider
によってインスタンスが提供されている。
<?php // `BEAR\Resource\Module\SchemeCollectionProvider` から抜粋 // (snip) public function get() { $schemeCollection = new SchemeCollection; $pageAdapter = new AppAdapter($this->injector, $this->appName); $appAdapter = new AppAdapter($this->injector, $this->appName); $schemeCollection->scheme('page')->host('self')->toAdapter($pageAdapter); $schemeCollection->scheme('app')->host('self')->toAdapter($appAdapter); return $schemeCollection; } // (snip)
とまぁこんな感じで、 BEAR\Resource\AppAdapter
を見ればいいんだなってことが何となく分かる。
<?php // `BEAR\Resource\AppAdapter`から抜粋 // (snip) public function get(AbstractUri $uri) { if (substr($uri->path, -1) === '/') { $uri->path .= 'index'; } $path = str_replace('-', '', ucwords($uri->path, '/-')); $class = sprintf('%s%s\Resource\%s', $this->namespace, $this->path, str_replace('/', '\\', ucwords($uri->scheme) . $path)); try { $instance = $this->injector->getInstance($class); } catch (Unbound $e) { throw $this->getNotFound($uri, $e, $class); } return $instance; } // (snip)
はい、ここは結構重要だと思ったところ。
ここで、 Uri('app://self/path/to')
を元にしたクラス名に変換され、
それを DI から取得している事がわかる。
この場合は、
MyVendor\MyPackage\Resource\App\Path\To
になるので、このインスタンスが取得される。
ちなみに、これらのリソースオブジェクトをDIに束縛しているのは、BEAR\Package\Provide\Resource\ResourceObjectModule
みたい。
さぁ、ここまででやっと、BEAR\Resource\Resource::uri($uri)
内の処理である
<?php $resourceObject = $this->newInstance($uri);
が何をしているか分かった。
URIに基づくリソースオブジェクトクラスのインスタンスをDIから取得したわけだ。
しかしまだ実行はしていない。
で、これを BEAR\Resource\Request
に詰め込んで生成して返している。
この BEAR\Resource\Request
には実行すべきリソースオブジェクトと BEAR\Resource\Invoker
という、リソースオブジェクトを実行するインスタンスを持っているため、
あとはこれを実行すればよい。
実行するメソッドは親クラスである BEAR\Resource\AbstractRequest
にある。
<?php // `BEAR\Resource\AbstractRequest`から抜粋 // (snip) public function __invoke(array $query = null) { if ($query !== null) { $this->query = array_merge($this->query, $query); } if ($this->links) { return $this->linker->invoke($this); } return $this->invoker->invoke($this); } // (snip)
引数にクエリパラメーターを取り、Invoker
を用いて実行している。
<?php // `BEAR\Resource\Invoker`から抜粋 // (snip) public function invoke(AbstractRequest $request) { $onMethod = 'on' . ucfirst($request->method); if (method_exists($request->resourceObject, $onMethod) !== true) { return $this->invokeOptions($request->resourceObject, $request, $onMethod); } if ($request->resourceObject->uri instanceof AbstractUri) { $request->resourceObject->uri->query = $request->query; $request->resourceObject->uri->method = $request->method; } $params = $this->params->getParameters([$request->resourceObject, $onMethod], $request->query); $result = call_user_func_array([$request->resourceObject, $onMethod], $params); return $this->postRequest($request, $result); } // (snip)
なんじゃかんじゃあるが、結局のところ重要なのは下記だけ。
<?php call_user_func_array([$request->resourceObject, $onMethod], $params)
リソースオブジェクトのonGet
などのメソッドは与えられた引数を元に、
自分自身の内部状態を変えて、自分自身(リソースオブジェクト)を返すことになっているので、
最終的にリソースオブジェクトが返る。
ここまでを振り返ってみると、
<?php $page = $app->resource->get->uri('app://self/path/to')(['key'=> 'value', 'hoge' => 'fuga']);
これは
<?php $resource = $app->resource; // BEAR\Resource\Resource $resource = $resource->get; // BEAR\Resource\Resource $request = $resource->uri('app://self/path/to'); // BEAR\Resource\Request $page = $request('key'=> 'value', 'hoge' => 'fuga']); // BEAR\Resource\ResourceObject
このように理解することが出来る。
$page->transfer()
は何をしている?
力尽きたのでまた今度・・。
$app->error->handle()
は何をしている?
力尽きたのでまた今度・・。
今回のまとめ
今回読んだ範囲での自分の理解をまとめてみます。
変数 | インターフェイスとクラス | 私の理解 |
---|---|---|
$app |
BEAR\Sunday\Extension\Application\AppInterface で実態は MyVendor\MyPackage\Module\App |
$contexts に応じ、DIが依存解決したオブジェクトグラフを持つアプリケーション |
$request |
BEAR\Sunday\Extension\Router\RouterMatch |
HTTPリクエストをBEARリソースへのメソッド,URI,パラメータに変換したもの |
$page |
BEAR\Resource\ResourceObject |
BEARリソースのメソッドとURIから、それに該当するリソースオブジェクトを取得し、それを実行した結果 |
$app
が面白いですね。全部そこにまとまっているっていうのが。
さてさて、それではまた。