解析その4
核心……、の、はず!!w
「Routeの__invoke」からのstartでございます。
vendor/slim/slim/Slim/Route.php
public function __invoke(ServerRequestInterface $request, ResponseInterface $response) { $this->callable = $this->resolveCallable($this->callable); /** @var InvocationStrategyInterface $handler */ $handler = isset($this->container) ? $this->container->get('foundHandler') : new RequestResponse(); $newResponse = $handler($this->callable, $request, $response, $this->arguments); if ($newResponse instanceof ResponseInterface) { // if route callback returns a ResponseInterface, then use it $response = $newResponse; } elseif (is_string($newResponse)) { // if route callback returns a string, then append it to the response if ($response->getBody()->isWritable()) { $response->getBody()->write($newResponse); } } return $response; }
面白そうなんで、ちと細かく見ていきますか。
$this->callable = $this->resolveCallable($this->callable);
resolveCallableメソッド………ないし。
use MiddlewareAwareTrait;
からあたりつけて vendor/slim/slim/Slim/MiddlewareAwareTrait.php ………いねぇ。
class Route extends Routable implements RouteInterface
だから継承元クラス、かな? vendor/slim/slim/Slim/Routable.php ………いねぇ。
Routableに
use CallableResolverAwareTrait;
なので
vendor/slim/slim/Slim/CallableResolverAwareTrait.php
/** * Resolve a string of the format 'class:method' into a closure that the * router can dispatch. * * @param callable|string $callable * * @return \Closure * * @throws RuntimeException If the string cannot be resolved as a callable */ protected function resolveCallable($callable) { if (!$this->container instanceof ContainerInterface) { return $callable; } /** @var CallableResolverInterface $resolver */ $resolver = $this->container->get('callableResolver'); return $resolver->resolve($callable); }
いたいた。
「Resolve a string of the format 'class:method' into a closure that the router can dispatch.」あ、ここか。いきなり「見つけたいものの一つ」を発見。
callableResolver が、処理の箇所的に怪しいなぁ。
念のために確認……
vendor/slim/slim/Slim/DefaultServicesProvider.php
$container['callableResolver'] = function ($container) { return new CallableResolver($container); };
うんクラス名そのまんま。
vendor/slim/slim/Slim/CallableResolver.php
public function resolve($toResolve) { if (is_callable($toResolve)) { return $toResolve; } if (!is_string($toResolve)) { $this->assertCallable($toResolve); } // check for slim callable as "class:method" if (preg_match(self::CALLABLE_PATTERN, $toResolve, $matches)) { $resolved = $this->resolveCallable($matches[1], $matches[2]); $this->assertCallable($resolved); return $resolved; } $resolved = $this->resolveCallable($toResolve); $this->assertCallable($resolved); return $resolved; }
はいジャストミート。
・呼べる形式(is_callable)ならそのまま
・class:methodなら「resolveCallable」してから「assertCallable」
ふむ……resolveCallableの中身から、かなぁ。
protected function resolveCallable($class, $method = '__invoke') { if ($this->container->has($class)) { return [$this->container->get($class), $method]; } if (!class_exists($class)) { throw new RuntimeException(sprintf('Callable %s does not exist', $class)); } return [new $class($this->container), $method]; }
あら。「コンテナにあるならそのクラスを使う」なんだ。めったに引っかからないとは思うんだけど「ものすごくレアに"同一インスタンス"であるためにはまる」とか、ありそうな気がするなぁ………もわっとした、漠然としたイメージだけど。ちょっとだけ気にしておこう。
あと、コンストラクタにcontainer渡してるのか。ふむり。
で、戻り値は「インスタンスとメソッド名」の配列、か。
デフォルトの引数の「$method = '__invoke'」も、ちょっとおもしろいなぁ……これに依存するコード書くと、微妙にトリッキーになりそうだけど。
お次、assertCallable。
protected function assertCallable($callable) { if (!is_callable($callable)) { throw new RuntimeException(sprintf( '%s is not resolvable', is_array($callable) || is_object($callable) ? json_encode($callable) : $callable )); } }
あぁ。assert、だからまんま、か。
ってことは、これで「インスタンスとメソッド名の配列」がreturnされるんだな。
なので
vendor/slim/slim/Slim/Route.php
$this->callable = $this->resolveCallable($this->callable);
の$this->callableには「インスタンスとメソッド名、の配列」が入ってくる、と。
/** @var InvocationStrategyInterface $handler */ $handler = isset($this->container) ? $this->container->get('foundHandler') : new RequestResponse();
foundHandler………なかったっけ?
vendor/slim/slim/Slim/DefaultServicesProvider.php
$container['foundHandler'] = function () { return new RequestResponse; };
あったあった。
んじゃ、RequestResponseを閲覧。
………あら? 直下にいない。findコマンドで探してみる。
$ find ./ -name RequestResponse.php
./vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php
あらためて
vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php
public function __invoke( callable $callable, ServerRequestInterface $request, ResponseInterface $response, array $routeArguments ) { foreach ($routeArguments as $k => $v) { $request = $request->withAttribute($k, $v); } return call_user_func($callable, $request, $response, $routeArguments); }
「call_user_func」うんだよねぇ。
微妙に
foreach ($routeArguments as $k => $v) { $request = $request->withAttribute($k, $v); }
の旨味が見えないんだけど、まぁとりあえず「requestのAttributeの中に、引数パラメタの内容($routeArguments)が入ってる」ってのをほんのりと記憶しておきませう。
あと。withAttributeって確か「インスタンスをclone」しているはずなんだよねぇ。あんまりパラメタ数が大きいとオーバヘッドとかかかりそうな気がせんでもないんだけど、その辺、どうなんだろうさね?
で
$newResponse = $handler($this->callable, $request, $response, $this->arguments);
ここで呼んでるんだよねぇ。
ってことは基本、呼ばれる各「Controller#action」相当の子は「ResponseInterfaceをreturnしなきゃいけない」って決まりがあるんだな。
if ($newResponse instanceof ResponseInterface) { // if route callback returns a ResponseInterface, then use it $response = $newResponse; } elseif (is_string($newResponse)) {
或いは
} elseif (is_string($newResponse)) { // if route callback returns a string, then append it to the response if ($response->getBody()->isWritable()) { $response->getBody()->write($newResponse); } }
「文字列をreturn」でもまぁOKで、その場合は「その文字列が設定される」と。
んで、最終的にいずれにしても「$responseインスタンス」がreturnされるのか。
で、ここから、Routeインスタンスとしての
vendor/slim/slim/Slim/MiddlewareAwareTrait.php
public function callMiddlewareStack(ServerRequestInterface $request, ResponseInterface $response) { if (is_null($this->tip)) { $this->seedMiddlewareStack(); } /** @var callable $start */ $start = $this->tip; $this->middlewareLock = true; $response = $start($request, $response); $this->middlewareLock = false; return $response; }
での$responseに入るんだな。
さて、逆追いしていこう。
vendor/slim/slim/Slim/Route.php
public function run(ServerRequestInterface $request, ResponseInterface $response) { // Finalise route now that we are about to run it $this->finalize(); // Traverse middleware stack and fetch updated response return $this->callMiddlewareStack($request, $response); }
だったので、ここもまたreturnで返っていくだけ、かな。
ふむ……なんだかんだ「呼ばれまくってる」ので、イメージがつきにくい。
あと「深く潜るほう」に追いかけてったので、戻りが、わかりにくい。
ちと後手に回ったけど、「callスタック」的な見地から、少し、まとめてみやう。色々と省略したりしているので、補助程度に見てくださいませ。
App#get() | App#post() | App#put() | App#delete() App#map() Route = App#container->get('router')->map($methods, $pattern, $callable) Route#setContainer() Route#setOutputBuffering() App#run() App#process() Router#setBasePath() App#dispatchRouterAndPrepareRoute() Route = Router#lookupRoute() Route#prepare() Request#withAttribute App#callMiddlewareStack() App#seedMiddlewareStack() $start(App) = App#tip $start(App) -> App#__invoke Router = App#container->get('router') Request = App#dispatchRouterAndPrepareRoute() Router#dispatch() Route = Router#lookupRoute Route#prepare() Request#withAttribute Request#getAttribute Route = Router#lookupRoute Route#run() Route#finalize() Route#addMiddleware() Route#callMiddlewareStack() Route#seedMiddlewareStack() $start(Route) = Route#tip $start(Route) -> Route#__invoke Route#resolveCallable Handler = Route#container->get('foundHandler') Response = Handler() call_user_func() (Http\Body#write) Response#getBody()->write() App#finalize() ini_set('default_mimetype', '') Response#getBody()->getSize() App#respond
さて戻ってきて「Http\Body#write」から再開。
vendor/slim/slim/Slim/Http/Body.php
class Body extends Stream { }
vendor/slim/slim/Slim/Http/Stream.php
public function write($string) { if (!$this->isWritable() || ($written = fwrite($this->stream, $string)) === false) { throw new RuntimeException('Could not write to stream'); } // reset size so that it will be recalculated on next call to getSize() $this->size = null; return $written; }
あら「fwrite($this->stream, $string)」なのか。
public function __construct($stream) { $this->attach($stream); }
protected function attach($newStream) { if (is_resource($newStream) === false) { throw new InvalidArgumentException(__METHOD__ . ' argument must be a valid PHP resource'); } if ($this->isAttached() === true) { $this->detach(); } $this->stream = $newStream; }
ありゃ………渡してる前提なのか。
vendor/slim/slim/Slim/App.php
$body = new Http\Body(fopen('php://temp', 'r+'));
なるほど。んじゃ理解したので一旦放置。
次。
$response = $response->withBody($body); } elseif ($outputBuffering === 'append') { // append output buffer content $response->getBody()->write($output); }
なので。
responseの「withBody」と「getBody()->write」について、だねぇ。
まずwithBody。
………ない。継承元の親クラスにいた。
vendor/slim/slim/Slim/Http/Message.php
public function withBody(StreamInterface $body) { // TODO: Test for invalid body? $clone = clone $this; $clone->body = $body; return $clone; }
この場合のthisはResponseだね。
一方のgetBody()->write。
getBodyはやっぱり継承元。
vendor/slim/slim/Slim/Http/Message.php
public function getBody() { return $this->body; }
body自体は
vendor/slim/slim/Slim/Http/Response.php
public function __construct($status = 200, HeadersInterface $headers = null, StreamInterface $body = null) { $this->status = $this->filterStatus($status); $this->headers = $headers ? $headers : new Headers(); $this->body = $body ? $body : new Body(fopen('php://temp', 'r+')); }
で入れてた。
ふむここも php://temp なのか。
おあとはwriteなので、処理的には一緒。
ラスト直前、finalize。
vendor/slim/slim/Slim/Appex.php
protected function finalize(ResponseInterface $response) { // stop PHP sending a Content-Type automatically ini_set('default_mimetype', ''); if ($this->isEmptyResponse($response)) { return $response->withoutHeader('Content-Type')->withoutHeader('Content-Length'); } // Add Content-Length header if `addContentLengthHeader` setting is set if (isset($this->container->get('settings')['addContentLengthHeader']) && $this->container->get('settings')['addContentLengthHeader'] == true) { if (ob_get_length() > 0) { throw new \RuntimeException("Unexpected data in output buffer. " . "Maybe you have characters before an opening <?php tag?"); } $size = $response->getBody()->getSize(); if ($size !== null && !$response->hasHeader('Content-Length')) { $response = $response->withHeader('Content-Length', (string) $size); } } return $response; }
「出力している」っぽい箇所がない。
念のため、returnの直前にexitを入れてみる………うん、出てこない。
んじゃ、出力はrespondかしらん。
/** * Send the response to the client * * @param ResponseInterface $response */ public function respond(ResponseInterface $response)
あたり、だ。
つまりfinalizeで「出力直前」まで準備して、出力はrespondでやってるのか。
では処理を追いかけてみませう。
// Send response if (!headers_sent()) { // Headers foreach ($response->getHeaders() as $name => $values) { foreach ($values as $value) { header(sprintf('%s: %s', $name, $value), false); } } // Set the status _after_ the headers, because of PHP's "helpful" behavior with location headers. // See https://github.com/slimphp/Slim/issues/1730 // Status header(sprintf( 'HTTP/%s %s %s', $response->getProtocolVersion(), $response->getStatusCode(), $response->getReasonPhrase() )); }
header関数使ってるのか。エスケープしてない………中身はHeadersクラスっぽい………。
ちょろっと寄り道。
vendor/slim/slim/Slim/Http/Headers.php
public function set($key, $value) { if (!is_array($value)) { $value = [$value]; } parent::set($this->normalizeKey($key), [ 'value' => $value, 'originalKey' => $key ]); }
public function normalizeKey($key) { $key = strtr(strtolower($key), '_', '-'); if (strpos($key, 'http-') === 0) { $key = substr($key, 5); } return $key; }
あぁ面白い事やってるなぁ。
ただ、値のエスケープとかフィルタリング*1とか、やってないような。
戻って、出力の残り。
// Body if (!$this->isEmptyResponse($response)) { $body = $response->getBody(); if ($body->isSeekable()) { $body->rewind(); } $settings = $this->container->get('settings'); $chunkSize = $settings['responseChunkSize']; $contentLength = $response->getHeaderLine('Content-Length'); if (!$contentLength) { $contentLength = $body->getSize(); } if (isset($contentLength)) { $amountToRead = $contentLength; while ($amountToRead > 0 && !$body->eof()) { $data = $body->read(min($chunkSize, $amountToRead)); echo $data; $amountToRead -= strlen($data); if (connection_status() != CONNECTION_NORMAL) { break; } } } else { while (!$body->eof()) { echo $body->read($chunkSize); if (connection_status() != CONNECTION_NORMAL) { break; } } } }
なんか丁寧な出力の仕方だなぁ。
まぁ極論でいうと「echoで出力している」まる。
ふむ、とりあえず「頭からケツまで」の大体の流れは見えてきたかなぁ。
次は
・slim/slim-skeleton の解析(含む autoloader 周り):「やってること」と「できる事」と「多分公式が推奨してるんじゃないかと思われる方向性」の確認
・https://www.slimframework.com/docs/ のめぼしいところをつまみ食い
→ Middlewareの確認(と、必要そうなら解析)、ルーティングのgroups
→ jsonの出力
→ validate
あたり。つまみ食い関連は、上述以外でも面白そうなのがあれば、色々。
この辺が一通り落ち着いたら
・どんな風に書くか:おいちゃん流
を少しづつ作っていって、頑張れそうなら
・MagicWeaponとの悪魔合体
を試みてみませうw
*1:いらん値の除去、くらいのニュアンス