gallu’s blog

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

解析その2

さて。本丸のrun()メソッド。
多分、ここからが長丁場な予感(笑


vendor/slim/slim/Slim/App.php

    public function run($silent = false)
    {
        $response = $this->container->get('response');

        try {
            ob_start();
            $response = $this->process($this->container->get('request'), $response);
        } catch (InvalidMethodException $e) {
            $response = $this->processInvalidMethod($e->getRequest(), $response);
        } finally {
            $output = ob_get_clean();
        }

        if (!empty($output) && $response->getBody()->isWritable()) {
            $outputBuffering = $this->container->get('settings')['outputBuffering'];
            if ($outputBuffering === 'prepend') {
                // prepend output buffer content
                $body = new Http\Body(fopen('php://temp', 'r+'));
                $body->write($output . $response->getBody());
                $response = $response->withBody($body);
            } elseif ($outputBuffering === 'append') {
                // append output buffer content
                $response->getBody()->write($output);
            }
        }

        $response = $this->finalize($response);

        if (!$silent) {
            $this->respond($response);
        }

        return $response;
    }

あ、シンプル。
………引数の$silentが気になるなぁ。まぁ置いといて。


ふむ………基本的には
・$this->process
・$output = ob_get_clean();
・$response = $response->withBody($body); または $response->getBody()->write($output);
・$response = $this->finalize($response);
・$this->respond($response);
こんな感じか。


・$response->getBody()->isWritable()
・$this->container->get('settings')['outputBuffering'];
あたりが個別に気になるなぁ。………なんとなく、初手の「$response = $this->container->get('response');」あたりが気がかりになるんだけど(このタイミングではfalseとかじゃないんだろうか?)。
まぁ。順繰りに追いかけてみませう。


まずは「$this->process」の解析が先決かな。
大まかには「process(request, response)」なので、大体予想がつきそうな処理ではある、し、元々「一番見たかった」場所でもありそうだ。

    public function process(ServerRequestInterface $request, ResponseInterface $response)
    {
        // Ensure basePath is set
        $router = $this->container->get('router');

引数でちゃんと型固定、ふむふむ好み。
次、routerインスタンスの取り出し。

        if (is_callable([$request->getUri(), 'getBasePath']) && is_callable([$router, 'setBasePath'])) {
            $router->setBasePath($request->getUri()->getBasePath());
        }

ふむ………少し理解不能
まず「$request->getUri()」をvar_dump()。

object(Slim\Http\Uri)#18 (9) {
["scheme":protected]=>
string(4) "http"
["user":protected]=>
string(0) ""
["password":protected]=>
string(0) ""
["host":protected]=>
string(12) "domain.com"
["port":protected]=>
int(8080)
["basePath":protected]=>
string(0) ""
["path":protected]=>
string(11) "/hello/furu"
["query":protected]=>
string(0) ""
["fragment":protected]=>
string(0) ""
}

あぁうん、普通にURIの情報か。
ふむ………少し一足飛びに「ifの中の二つのis_callableのreturnを確認。

bool(true)
bool(true)

trueか………少し細かくほじくってみよう………あぁそうか単純に「[0]のインスタンスに[1]のメソッドがあるか」の確認か。
method_existsと何が違うんだろ………あ、そか。念のため実験。

<?php

class hoge {
    public function aaa() {}
    protected function bbb() {}

}

$obj = new hoge();
var_dump( method_exists($obj, 'aaa') );
var_dump( method_exists($obj, 'bbb') );
var_dump( is_callable([$obj, 'aaa']) );
var_dump( is_callable([$obj, 'bbb']) );

bool(true)
bool(true)
bool(true)
bool(false)

ふむ予想通り。
method_existsは「メソッド存在の有無」、is_callableは「呼べるメソッドか?」なのか。
はぁこの辺、作りが丁寧だなぁ。少し見習いましょう。


話を戻して。

        if (is_callable([$request->getUri(), 'getBasePath']) && is_callable([$router, 'setBasePath'])) {
            $router->setBasePath($request->getUri()->getBasePath());
        }

はおおざっぱに
・RouterインスタンスにBasePath情報をセット
してるのか………ここがfalseだったらどうすんだべ? とか思うんだけど、まぁ気にせず。
念のため、Routerのほうを軽く確認。


vendor/slim/slim/Slim/Router.php

    public function setBasePath($basePath)
    {
        if (!is_string($basePath)) {
            throw new InvalidArgumentException('Router basePath must be a string');
        }

        $this->basePath = $basePath;

        return $this;
    }

はいありがとうございます予想どおり(笑
念のため値を確認。

string(0) ""

………あら? ちょいと予想からはずれた。
気になるというよりは興味があるので、軽く探ってみませう。


vendor/slim/slim/Slim/Http/Request.php

    public function getUri()
    {
        return $this->uri;
    }

シンプル。
ここで返ってくるのが「Slim\Http\Uri」だったので。
vendor/slim/slim/Slim/Http/Uri.php

    public function getBasePath()
    {
        return $this->basePath;
    }

あら、まんま。
grep -R setBasePath vendor/slim/slim/Slim/*
grep -R setBasePath *
で捜索しても、あんまり出てこない………なんとなく「ドメイン直下じゃなくて、どこかディレクトリ掘ってWebアプリケーションを置く」ような時に使う、全体用の子、っぽくみえるなぁ。


まぁ、幾分気になるものの*1、一旦、次に進めてみましょう。


vendor/slim/slim/Slim/App.php

        // Dispatch the Router first if the setting for this is on
        if ($this->container->get('settings')['determineRouteBeforeAppMiddleware'] === true) {
            // Dispatch router (note: you won't be able to alter routes after this)
            $request = $this->dispatchRouterAndPrepareRoute($request, $router);
        }

多分ここが本命中の本命だろうなぁ。
……まず「settings」ってなんじゃらほい?

object(Slim\Collection)#20 (1) {
  ["data":protected]=>
  array(7) {
    ["httpVersion"]=>
    string(3) "1.1"
    ["responseChunkSize"]=>
    int(4096)
    ["outputBuffering"]=>
    string(6) "append"
    ["determineRouteBeforeAppMiddleware"]=>
    bool(false)
    ["displayErrorDetails"]=>
    bool(false)
    ["addContentLengthHeader"]=>
    bool(true)
    ["routerCacheFile"]=>
    bool(false)
  }
}

あぁなんか「基本的な設定が入ってる」ぽいなぁ。


vendor/slim/slim/Slim/DefaultServicesProvider.php
をチェック……あれ? newしてる箇所がない。


vendor/slim/slim/Slim/Container.php

    public function __construct(array $values = [])
    {
        parent::__construct($values);

        $userSettings = isset($values['settings']) ? $values['settings'] : [];
        $this->registerDefaultServices($userSettings);
    }

ふむ「指定されてたらそれを、指定されてなかったら空の配列を」………

    private function registerDefaultServices($userSettings)
    {
        $defaultSettings = $this->defaultSettings;

        /**
         * This service MUST return an array or an
         * instance of \ArrayAccess.
         *
         * @return array|\ArrayAccess
         */
        $this['settings'] = function () use ($userSettings, $defaultSettings) {
            return new Collection(array_merge($defaultSettings, $userSettings));
        };

        $defaultProvider = new DefaultServicesProvider();
        $defaultProvider->register($this);
    }

あぁ「defaultSettingsとuserSettingsをいい感じに足しっぱなしなCollectionを作る」のかなるほど。
array_mergeだから「上書き系」なので「defaultSettingsに値があってもuserSettingsにも設定があったらuserSettingsが優先になる」んだねぇ、というあたりを読み取っていくのが基本w


ついでだから defaultSetting についても軽く覗いてみよう。

    private $defaultSettings = [
        'httpVersion' => '1.1',
        'responseChunkSize' => 4096,
        'outputBuffering' => 'append',
        'determineRouteBeforeAppMiddleware' => false,
        'displayErrorDetails' => false,
        'addContentLengthHeader' => true,
        'routerCacheFile' => false,
    ];

………ふぁ?
あぁそうか「デフォルト」だからいいのか。これ、上書きできないw
てっきりsetDefaultSettings的なメソッドを期待していたんだけど、それやりたいなら「userSettingsで指定しろよ」って話だから不要なのか。理解。


デフォルトの挙動を変えたい場合は
・Containerクラスをnewする時、引数に$values['settings']のhash配列として値をいれてやる
・このContainerクラスをAppのnewん時に渡してやる
………ふむ、前に書いてた「Appを一旦デフォでnewしてから後で上書き」と少しずれるなぁ。
やり口としては
・Appをデフォでnew
・その後「$app->container->get('settings')['hoge'] = 'fuga';」とか
のほうが楽かもしらん…… slim_test/src/settings.php になんかあるし、そっち見て詳しいところは把握するんだろうなぁ。


あと。Collectionクラス、なんか見なくてもよさそうだけど、一応。
vendor/slim/slim/Slim/Collection.php
……ほいほい。なんとなし「arrayObjectではダメなん?」とか思うんだけど、なんかあるんだろうなぁ(未確認)。
とりあえず「いわゆる配列をObjectで扱えるクラス」的な感じなので、一旦その辺で掘り込みstop。
ってなわけで、現状は放置w


改めて
vendor/slim/slim/Slim/App.php

        // Dispatch the Router first if the setting for this is on
        if ($this->container->get('settings')['determineRouteBeforeAppMiddleware'] === true) {
            // Dispatch router (note: you won't be able to alter routes after this)
            $request = $this->dispatchRouterAndPrepareRoute($request, $router);
        }

………あれ? 値がfalse。
あぁ「routeの前にMiddleware アプリケーションが定められていれば」フラグ、か。
多分、Middlewareの設定があるときに使われるんだな。
んじゃ、後回しw ………


次。

        try {
            $response = $this->callMiddlewareStack($request, $response);
        } catch (Exception $e) {
            $response = $this->handleException($e, $request, $response);
        } catch (Throwable $e) {
            $response = $this->handlePhpError($e, $request, $response);
        }

って事は、callMiddlewareStack が本命かな??
………むぅメソッドがない。

    use MiddlewareAwareTrait;

かぁ。
findしてファイル位置特定(この辺、手抜きw)。


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;
    }

いたいた。
………ふむよぉ分からんなぁ。
とりあえず、直線的にいきますか。
「$this->tip」は……nullか。したらseedMiddlewareStackですな。

    protected function seedMiddlewareStack(callable $kernel = null)
    {
        if (!is_null($this->tip)) {
            throw new RuntimeException('MiddlewareStack can only be seeded once.');
        }
        if ($kernel === null) {
            $kernel = $this;
        }
        $this->tip = $kernel;
    }

………ふむぅ何かを入れている。っつか何かが「$this」だから、今読んでいる文脈だと「Slim\App」が入るのか。

        $start = $this->tip;
        $this->middlewareLock = true;
        $response = $start($request, $response);
        $this->middlewareLock = false;

………ん?
ってことは「Appのインスタンスを関数のようにcallしている」??
えと………__invoke、か。
うん、Appにあったあった。


vendor/slim/slim/Slim/App.php

    public function __invoke(ServerRequestInterface $request, ResponseInterface $response)
    {
        // Get the route info
        $routeInfo = $request->getAttribute('routeInfo');

        /** @var \Slim\Interfaces\RouterInterface $router */
        $router = $this->container->get('router');

        // If router hasn't been dispatched or the URI changed then dispatch
        if (null === $routeInfo || ($routeInfo['request'] !== [$request->getMethod(), (string) $request->getUri()])) {
            $request = $this->dispatchRouterAndPrepareRoute($request, $router);
            $routeInfo = $request->getAttribute('routeInfo');
        }

        if ($routeInfo[0] === Dispatcher::FOUND) {
            $route = $router->lookupRoute($routeInfo[1]);
            return $route->run($request, $response);
        } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) {
            if (!$this->container->has('notAllowedHandler')) {
                throw new MethodNotAllowedException($request, $response, $routeInfo[1]);
            }
            /** @var callable $notAllowedHandler */
            $notAllowedHandler = $this->container->get('notAllowedHandler');
            return $notAllowedHandler($request, $response, $routeInfo[1]);
        }

        if (!$this->container->has('notFoundHandler')) {
            throw new NotFoundException($request, $response);
        }
        /** @var callable $notFoundHandler */
        $notFoundHandler = $this->container->get('notFoundHandler');
        return $notFoundHandler($request, $response);
    }

ここが本気の本命っぽいなぁ。


ちぃと長くなったので、以降は次回w

*1:どこで設定するん? とかあるし