gallu’s blog

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

解析その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:いらん値の除去、くらいのニュアンス