がるの健忘録

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

解析その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:どこで設定するん? とかあるし

解析その1

一瞬「解析シリーズ」とかやってみようかしらん? とか、思ってみたりみなかったり。


おいといて。


さて、ゆるゆると解析開始。
まぁ大概のWebアプリケーション、最近は「ここから始まる」1点があるので、そこを確認。
よっぽどヒネてない限り、大概は「DocumentRoot直下のindex.php」が、その1点になります。


とりあえず流れを把握したいので。
ざっくりと public/index.php の最後に

var_dump( debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT|DEBUG_BACKTRACE_IGNORE_ARGS) );

を入れてみる……うんダメだ出てこない出力されない。最近このパターン多いなぁ*1ついでに色々と解明しよう後日。いやまぁ予想はしてた。


次。

ob_start();
var_dump( debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT|DEBUG_BACKTRACE_IGNORE_ARGS) );
$s = ob_get_clean();
file_put_contents(__DIR__ . '/log', $s);

………うんこれもだめ。空配列が返ってくる。やっぱり「奥地でcallしないと」全部を追えるわけじゃないのか。


一応実験(slim_test_plainにて)。

$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
    $name = $args['name'];

ob_start();
var_dump( debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT|DEBUG_BACKTRACE_IGNORE_ARGS) );
$s = ob_get_clean();

    $response->getBody()->write("Hello, $name\n" . $s);


    return $response;
});

うん、これなら出る。………ただ、ちょっとやっぱりボリューミーだなぁ。読むのにへこたれそうだw


んじゃまぁ、横着せず、頭から読んでいきますかねぇ。
どっちでも(このレベルでは)差異はないと思われるので、 slim_test_plain を基準に一旦読み込んでみる。シンプルで読みやすいだろうし。
public/index.phpから直接読み取れるのは
・new \Slim\App;
・getメソッド
 →第三引数のcallableの引数で「Request, Response」
 →ResponseのgetBody()からのwrite()
 →Responseインスタンスをreturn
・run()メソッド


とりあえず\Slim\Appクラスの
・getメソッドを軽く眺めてから
・runメソッドを一旦追いかける
くらい、が、手掛かりの初手かなぁ。
まぁ、まずはgetメソッドをほじくってみませう。


vendor/slim/slim/Slim/App.php

    public function get($pattern, $callable)
    {
        return $this->map(['GET'], $pattern, $callable);
    }

ふむぅ。第二引数「$callable」なんだ。確か「クラス+メソッド名」って渡し方ができるはずなので。どこかで分解しているのかねぇ。
ちなみにその下に

    public function post($pattern, $callable)
    {
        return $this->map(['POST'], $pattern, $callable);
    }

とかあってPUTとかPATCH(うわぉ)とかDELETEとかOPTIONS(ほほぉ)とかあるので、処理一緒ぽ。

    public function any($pattern, $callable)
    {
        return $this->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $pattern, $callable);
    }

これもあってまぁわかりやすい。


本体はmapぽいので、mapを軽く閲覧。

        if ($callable instanceof Closure) {
            $callable = $callable->bindTo($this->container);
        }

あぁなるほど。無名関数形式で呼ばれたら「containerってやつのクラス」にbindするんだ。面白い。
……$this->containerってなんじゃらほい?

        $route = $this->container->get('router')->map($methods, $pattern, $callable);
        if (is_callable([$route, 'setContainer'])) {
            $route->setContainer($this->container);
        }

        if (is_callable([$route, 'setOutputBuffering'])) {
            $route->setOutputBuffering($this->container->get('settings')['outputBuffering']);
        }

        return $route;

あたりみても、$this->containerが前面っつか全面に出てる感じぽ。こいつのget()とかmap()とかあたりがkeyぽい。
ので、軽く確認。

    public function __construct($container = [])
    {
        if (is_array($container)) {
            $container = new Container($container);
        }
        if (!$container instanceof ContainerInterface) {
            throw new InvalidArgumentException('Expected a ContainerInterface');
        }
        $this->container = $container;
    }

ふむぅ……

use Psr\Container\ContainerInterface;

はあるんだけど、Containerってクラスは見当たらんなぁ。
require 'vendor/autoload.php'; との合わせ技で考えると一番高い可能性は「同じ名前空間の中にクラスがある」可能性………ほらあった。
vendor/slim/slim/Slim/Container.php


ただ、これって「Psr\Container\ContainerInterface」で予想される通り「PSR-11 Container Interface」なんだよねおそらく。
つまり「依存性注入コンテナの共通インタフェース」なので、突き詰めると「色々なインスタンスを抱え込んでるだけの子」。
いや仕組みとしてはぶっちゃけると「パクりたくなるくらい面白い」んだけど、今回の場合「そのインスタンスの中身」を知りたいので、ってことは「$this->container->get('router')」で取得できるクラスの情報が欲しいんだよね。


ちゃんと追いかけてもよいんだけど、ちょいとずるしながら追いかけてみる。
vendor/slim/slim/Slim/App.php

    public function map(array $methods, $pattern, $callable)
    {
        if ($callable instanceof Closure) {
            $callable = $callable->bindTo($this->container);
        }
var_dump( get_class($this->container->get('router')) ) ;
exit;

string(11) "Slim\Router"

うんやっぱりそこか。


軽く確認。

grep "new Router" vendor/slim/slim/Slim/*

vendor/slim/slim/Slim/DefaultServicesProvider.php: $router = (new Router)->setCacheFile($routerCacheFile);

DefaultServicesProviderですか気になるなぁ。

grep "DefaultServicesProvider" vendor/slim/slim/Slim/*

vendor/slim/slim/Slim/Container.php: $defaultProvider = new DefaultServicesProvider();
vendor/slim/slim/Slim/DefaultServicesProvider.php:class DefaultServicesProvider


vendor/slim/slim/Slim/Container.php

    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);
    }
    public function __construct(array $values = [])
    {
        parent::__construct($values);

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

ふむ、大体つながったざっくりだけど。
あと、DefaultServicesProviderのregisterだけ、覗き見。
………長いなぁ。
要約すると「設定されていなければ、デフォルトのクラスを紐づけておく」感じ。
environment
request
response
router
foundHandler
phpErrorHandler
errorHandler
notFoundHandler
notAllowedHandler
callableResolver
概ね「newするくらい」なんだけど、クラスによっては「ちょっと色々やってる(responseとか)」ので、軽く覗いてみると面白いかも。


あと。
Appのコンストラクタに

    public function __construct($container = [])
    {
        if (is_array($container)) {
            $container = new Container($container);
        }

ってあるから「なんか追加したかったら、Appのコンストラクタに渡せ」って感じだよね。
或いは「newしたあと、動作させる前に上書き」でもよいだろうし。


上書きは、実際
../slim_test/public/index.php
経由
../slim_test/src/dependencies.php
に処理が書いてあるので、参考になるんだと思う。
「newするときにコンストラクタに渡す」のと「newした後で$app->getContainer()で受け取ったインスタンスに書き込む」のとどっちがよいのかなぁ? とは思うんだけど、まぁ「newはAppにしてもらう」ほうが、楽は楽なのやもしれぬ。


さて。
盛大に回り道をしたので、元に戻してRoute周りを改めて。
発端は
vendor/slim/slim/Slim/App.php

    public function map(array $methods, $pattern, $callable)
    {
        if ($callable instanceof Closure) {
            $callable = $callable->bindTo($this->container);
        }

        $route = $this->container->get('router')->map($methods, $pattern, $callable);
        if (is_callable([$route, 'setContainer'])) {
            $route->setContainer($this->container);
        }

        if (is_callable([$route, 'setOutputBuffering'])) {
            $route->setOutputBuffering($this->container->get('settings')['outputBuffering']);
        }

        return $route;
    }

改めて「map」についてみてみる。
vendor/slim/slim/Slim/Router.php
のmapメソッド。
色々面白いんだけどとりあえずcreateRouteに焦点。

    protected function createRoute($methods, $pattern, $callable)
    {
        $route = new Route($methods, $pattern, $callable, $this->routeGroups, $this->routeCounter);
        if (!empty($this->container)) {
            $route->setContainer($this->container);
        }

        return $route;
    }

Routeクラスか。………確かあったな。
vendor/slim/slim/Slim/Route.php

    public function __construct($methods, $pattern, $callable, $groups = [], $identifier = 0)
    {
        $this->methods  = is_string($methods) ? [$methods] : $methods;
        $this->pattern  = $pattern;
        $this->callable = $callable;
        $this->groups   = $groups;
        $this->identifier = 'route' . $identifier;
    }

はいありがとうございます身もふたもないw
まぁ、恐らく後でまたもう一度、ちゃんと読みに来ることでしょう。


っつわけでとりあえず、$appのgetとかpostとかのいわゆる「ルーティングを突っ込むところ」では
・Containerの中に色々なインスタンスが入っている中のRouterクラスで処理する
 →Containerは、デフォルトで「DefaultServicesProvider」ってクラスがいろんなインスタンスを突っ込んでおいてくれている。使っても使わなくても。
 →Containerは、Appクラスのコンストラクタで。引数にあればそれ使うし、なかったら内部で適宜作成してくれる
・Routerで色々処理をするけど、概ね「1ルーティング」が「1Routeクラスインスタンス」ん中に情報突っ込まれている


ところまでが判明しました。
さて、次回は、本番である「run()」メソッドの切込みにはいります。
………何回くらいになるのかなぁ。

*1:Responseクラスの解析あたり周りくらい、でやると思いますっていう予想

Slim初めてみました

ちと色々と思考して、もしかしてSlimは割と「好みな可能性」を想起したもので。
いったん、分解して色々と整理してみたいなぁ、と思いました的、宣言エントリ。


いったん、環境だけ作成しました。
slim-skeleton使ったのと、ほぼ純正にSlimのみ、の2環境。
多分可能性として「slim-skeletonを選択する」可能性が高いとは思ってるのですが。
動きを理解するためにも「plainなSlim」をいじっておいた方がいいだろうなぁ、という判断でございます。


環境構築手順。
手元に composer.phar がある前提で。


slim-skeleton

php composer.phar create-project slim/slim-skeleton slim_test
cd slim_test
chmod 777 logs


plainなSlim

mkdir slim_test_plain
cd slim_test_plain
mkdir public
vi public/index.php
```
<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;

require 'vendor/autoload.php';

$app = new \Slim\App;
$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
    $name = $args['name'];
    $response->getBody()->write("Hello, $name");

    return $response;
});
$app->run();
```
php ../composer.phar require slim/slim "^3.0"


いずれも

php -S 0.0.0.0:8080 -t public public/index.php

で、ごく簡単に起動確認などしてみました。


ここからほじくっていって
・inからoutまで、どんな動きをするか
を、少しコードとか追いかけながら見ていきたいなぁ、と。
いや、テンプレートエンジンとか使いたいので「なんでこーゆー風にかくとちゃんと動くのか」とか、理解しておきたいですしおすし。


その辺の解明が終わったら……余力があったら
・DBを含むまるっとした一式の実験
・MagicWeaponとの悪魔合体
あたりにチャレンジしてみたい気が満々でございますw


まずは「Blogに書いておけば逃げ道がなくなるだろう」という身も蓋もないエントリーを、一発目に。

password_hashをどうやって使おうか?(04/15修正)

なんか最近「パスワード、いくつかの単語を組み合わせた長い文字列のほうが安全だよねぇ」的なお話が云々。
それを考えた時、今まで割と気にならなかった「警告 PASSWORD_BCRYPT をアルゴリズムに指定すると、 password が最大 72 文字までに切り詰められます。」が、途端に気になりだしてきたり。


ふと考える2つのコード、果たしてどっちがベターなのだろう?
或いは、変わらんかなぁ?
一応おいちゃんの中にはイメージがあるんだけど、ふと、問いかけてみたいように思ったので、書いてみる。

どうも、どっちも危ないっぽい。後述を参照のこと。


コード1(アウト)

$pass = パスワード;
if (72 < strlen($pass)) {
    $pass = hash('sha512', $pass, true);
}
$h = password_hash($pass, PASSWORD_DEFAULT, ['cost' => 10]);


コード2(アウト)

$h = password_hash(hash('sha512', $pass, true), PASSWORD_DEFAULT, ['cost' => 10]);


costの明示指定は趣味だ気にスンナw*1


さて。コード1とコード2、どっちがどうなんだろう???


追記
徳丸さんがfacebookで書かれていたんだけど。
「password_hash 関数はバイナリセーフでない」んだそうだ……………え?まぢで?
徳丸さんが載せていた、検証コード。

$password = "LmUb9dTqXUTTCuCxzCct3n2lIf/zS3IeZ15mtCivl0TQlD97YZtxK+u0j4AploaErAu9uYBBfqNs4R2nQITnyq+nuLZd";
$crack = "ma";
// Use sha512's raw(binary) output
$h = password_hash(hash('sha512', $password, true), PASSWORD_DEFAULT);

// Verify
$r = password_verify(hash('sha512', $crack, true), $h);

var_dump($r); // TRUEが返る

うん通った orz
コード1なら「上述は防げる」けど、「72文字を超える文字列同士で「任意箇所に\0が入ってくる」なら突破できるから、あんまりよろしくないぽげ。


さて思案。
「バイナリが駄目なら、base64にすればいいぢゃない」とか思うんだけど、sha512はbase64にしても88バイト使うから駄目なんだよねぇ。
ってなわけで、おとなしく素直に「16進数表記のsha256」が落としどころかなぁ、と。とりあえずは。


改めてコード1

$pass = パスワード;
if (72 < strlen($pass)) {
    $pass = hash('sha256', $pass);
}
$h = password_hash($pass, PASSWORD_DEFAULT, ['cost' => 10]);


改めてコード2

$h = password_hash(hash('sha256', $pass), PASSWORD_DEFAULT, ['cost' => 10]);


このどっちか、になるんかなぁ今の所だと。

*1:いや実際のところ、12くらいを指定する事が多くなってきたしねぇ

複合主キーの扱い方(一部)

LaravelのModelである「Eloquent ORM」で、複合主キーを扱う時の知見を得たので、備忘録的に。
Laravel5.5でやってます。それ以外のバージョンでどうなるかは不明です。


前提として。
おいちゃんは基本「サロゲートキー基本的にお好まない」一派です。
http://d.hatena.ne.jp/gallu/20160210/p1
この辺は議論がまだ色々ホットだと思うのですが、とはいえつまり「ホット」な程度に「どっちかは微妙」で。
なので基本的には「使えるもんなら使いたい」というスタンスです。
「複合主キーが、ちゃんとした理由でお好まないからサロゲートキー」ならよいのですが。
フレームワークの都合」だけでサロゲートキーを押しつけられるのは、どうにもお好まないのですだよねぇ。それをされると「じゃぁそのフレームワークを使わない、って選択肢」を考えてしまうので。


なので「使ってみました」的なエントリーでございます。


初手として。
マイグレーションファイルでの「複合主キーを含むテーブルの作成」は、簡単にできます。

$table->primary(['id', 'parent_id']);


Modelでは

protected $primaryKey = ['id', 'parent_id'];
public $incrementing = false;

でOK。下のは「なくても動いた」んだけど、何となく書いておきたいかなぁ、程度に。
keyType とかどうなるんじゃらほい? とか思うんだけど、そもこのModelに「::find(key)」とかやらないと思われるので、一旦気にしない。


んで。
例えば「データが欲しい」とか「key指定してupdateしたい」とかあるわけなのですが。
これで

$data = HogeModel::where('id', $id)->where('parent_id', $parent_id)->get()->toArray();

とか

$data = HogeModel::where('id', $id)->where('parent_id', $parent_id)->update($data);

とかやると、「Illegal offset type」とか怒られます。


結論から書くと「個別にwhereするとNGで、まとめてwhereするとOK」になります。

$data = HogeModel::where([ ['id', $id], ['parent_id', $parent_id] ])->get()->toArray();
$data = HogeModel::where([ ['id', $id], ['parent_id', $parent_id] ])->update($data);


あと、upsert的な事を(Modelクラスの中で)やるときは

$obj = $this->where([ ['id', $id], ['parent_id', $parent_id] ]);
if (null === $obj->first()) {
     // insert処理
     $this->insert($data);
} else {
     // update
     $obj->update($data);
}

ってな感じをベースにコネコネしていくと、まぁどうにかなるんじゃないかなぁ、と思われるです。
……これにちょっとはまった orz


「もう一度使う知識」かどうかは限りなく謎ですが。
まぁせっかく得た知見なので、共有まで。


……いやぶっちゃけもう結構「泥臭い力技」を覚悟してたので。
まぁどうにかなってよかったですだす。

foreachとか使わないのかしらん?

定期的に見かけるんだけど、今日もふと見かけたので、割と本気で疑問なので一度書いておこうかなぁ、と。
おいちゃん的には「foreachでくるんだらほんの少しだけ楽ぢゃない?」って思うようなコードが割とコピペで書かれているのを散見するので、「なんか理由があるのか」「とくに理由がないのか」興味もありまして、的な。


ん……典型的にわかりやすいのは、例えば、以下。

$data['hoge'] = $awk['hoge'];
$data['foo'] = $awk['foo'];
$data['bar'] = $awk['bar'];

面倒だから3行しか書いてないけど、これが10行以上とか、わりとざらに拝見。
おいちゃんなら

$params = ['hoge', 'foo', 'bar'];
foreach($params as $p) {
    $data[$p] = $awk[$p];
}

って書くかなぁ。
各項目ごとにちゃんとコメントが入ってるんなら

$params = [
    'hoge', // コメント
    'foo', // コメント
    'bar', // コメント
];

こげな感じで。


例えば「入れる側にprefixとかsuffixとか入れたい」なんてケースもあると思うのよ。

foreach($params as $p) {
    $data[$p . $suffix] = $awk[$p];
}

で終了


「いやいやそうじゃなくて配列じゃなくて変数名として使いたい」ってケースも、時々。
コード的にはこげな感じ。

$hoge = $awk['hoge'];
$foo = $awk['foo'];
$bar = $awk['bar'];


これは

foreach($params as $p) {
    $$p = $awk[$p];
}

で片付く。$paramsの中身が「ちゃんと変数名としてvalidである必要」はあるんだけどね。


これらの利点って。
単純に「項目が増えたり減ったり」、あと「個々の要素になにかひと手間加えたりしたくなった」時に、地味に便利なんじゃないかなぁ、って思うの。
typoの可能性が「原理的に0になる」し、わずかとはいえ「手間が省ける」のは、その手法を選択する「デメリット」がないのであれば、そっちのほうが楽なんじゃないかなぁ? と。
あと、個人的には「先頭に"処理対象を宣言"しておく」のが、可読性の観点から、読みやすいんじゃないかなぁ、と。まぁ単純に「おいちゃん好み」って言い方にしてもよいのだけど。


なので、この辺やらずにえっちらおっちらコピペしてるコードをみると、「……なんでだろ?」って疑問に思うのですだよ。
ってなわけで、興味があったのでメモり。
「いや実はこーゆー理由があってね」的な話があったら、きっと、コメントに書いてくれる人がいると、おいちゃんは信じてるw

Model、どうすっかねぇ? 的な

直近思案しているのはLaravel5.5案件なのですが。
まぁ割と「あちこちのPHP MVCフレームワークで言える(ような気がする)」ので、あちこちに疑問を投げかける的な想定で。
端的には「データの入力やvalidateの処理、Modelに書きますか? Controllerに書きますか?」的な内容です。


さて。いわゆるMVCの「Model」ですが。
本来的には
・アプリケーションデータ、ビジネスルール、ロジック:システムの本体
をつかさどる、とされています。
言い方を変えると「どのModelを使うかの制御(Controller)、表示や出力(View)」以外全部、をつかさどる、と言い伝えられています。


ただまぁ、どこのRoRが元凶とかActive Record以下検閲削除とかは言いませんが、半歩間違えると、Modelって
・ORマッパーのことでしょ?
的なお話になることも、少なからずあるように思われます。


いや別に「Model === ORM」だ、ってんならそれはそれでよいのですが。
その場合はまぁ「Controllerがfatになるのは避けられないよねぇ」とか思うわけです。
いやもちろん「一旦処理を外に書き出して"Controllerというファイル名のファイルの中身は細くする"」ってことも可能ですが、それって本当に「not Fat Controllerなの?」とか思うわけです。
「書き出したクラス」って、所属はController? Model?
「ControllerでもModelでもないそれ以外」だとしたら、それは本当にMVC? とかとか。


あぁ念のため「おいちゃん的には、トップハム・ハット卿 *1 を否定している」わけではありません。
いやまぁ肯定もとくにせんのですが。
その辺はぶっちゃけると「どっちでもいいんじゃないかなぁ一貫性があれば」。


正直、ある程度の規模のシステムを組めば
・太った子が出てくるか
・やたら大量の子だくさんになるか
のどちらかはどちらにしても不可避なので。ゆえに「ある程度納得できる理由と芯の通った哲学」があればよいのではないかなぁ、と思うのですます。


んで。「どっちでもよい」ので、一旦、仮定として「例えば、トップハム・ハット卿を避ける方向」で考えた場合。
Modelですが。「ビジネスロジック全部まるっと」はちぃと難しそうな気がせんでもないのですが、「あるデータの塊に対する責務全般」くらいは、お願いしてもよいのではないかなぁ? とか、おいちゃん個人としては思うのでございます。
ほら「Tell, Don't Ask」とか言うじゃないですかまぁ一般的なModelのコードもそんな感じだとは思うのですが。とはいえ、もうちょっと「いろいろ、Modelに持ち込んでもよいのかなぁ」とか、なんとか。


で、ちょいとちょうど良い感じがあったので、さっそくの実例。
ちと書いてもらったコードがあったので、微妙に細部を変えながら、転記。

    public function postTweet(Request $request)
    {
        $userId = auth()->id();
        if (null === $userId) {
            return redirect('/');
        }
        // ここはもうちょっと「$request->validate」つかうのが本当なんだろうと思う
        $tweetPost = $request->input('tweet');
        if (144 < strlen($tweetPost)) {
            return redirect('/');
        }

        $tweetModel = new Tweet();

        $tweetModel->user_id = $userId;
        $tweetModel->tweet = $tweetPost;
        $tweetModel->save();

        Session::flash('message', 'tweet した');
        return redirect('/');
    }

うんたぶん、LaravelでなくほかのPHP MVCフレームワークであっても、大体こんな感じだろうなぁと思うですサンプルコード見ている限り。
別の場所をグぐってみても。
http://libro.tuyano.com/index3?id=7896003&page=3

public function postNew(Request $request)
{
    $name = $request->input('name');
    $mail = $request->input('mail');
    $age = $request->input('age');
    $data = array(
        'name' => $name,
        'mail' => $mail,
        'age' => $age
    );
    MyTable::create($data);
    return redirect()->action('HeloController@getIndex');
}

https://qiita.com/yagi21/items/eea131ef0d3bc20be59a

  public function res(Request $request){
    //フォームから受け取る
    $フォームのname = $request->input('フォームのname');
      .
      .
      .
    //DB保存
  }

などなど。
公式も
https://readouble.com/laravel/5.5/ja/eloquent.html

class FlightController extends Controller
{
    /**
     * 新しいflightインスタンスの生成
     *
     * @param  Request  $request
     * @return Response
     */
    public function store(Request $request)
    {
        // リクエストのバリデート処理…

        $flight = new Flight;

        $flight->name = $request->name;

        $flight->save();
    }
}

なので、Laravel公式的にも「処理はControllerに書く」のほうに倒れてるんだろうなぁ、とは思うです。
さて上述に共通するのは「Controllerにデータ取得とvalidateの処理が書いてある」あたり。


んで、おいちゃんが気になっているのが
・入力データの取得
・validate
の処理を「Controllerでやっている」あたり。
その辺「どうなんだろう??」と思うですだす。


で、一案っつか二案なんだが。
上述の処理を「model側に移す」のって、みんなの印象値的にどうなんだろう? という、質問ですます。
1つは「全部まとめて1メソッド」、もう1つは「入力、validate、保存の3メソッド」の提案。
まぁModel内部的にはどちらにしても切り分けるんだろうけど。


1つまとめの場合は身もふたもなくて

    public function postTweet(Request $request)
    {
        $tweetModel = new Tweet();
        $r = $tweetModel->insert用に入力してvalidateして保存();
        if (true === $r) {
            Session::flash('message', 'tweet した');
        }
        return redirect('/');
    }


3つの場合は

    public function postTweet(Request $request)
    {
        $tweetModel = new Tweet();
        $r = $tweetModel->insert用の入力();
        if (false === $r) {
            return redirect('/');
        }
        $r = $tweetModel->insert用のvalidate();
        if (false === $r) {
            return redirect('/');
        }
        $r = $tweetModel->insert保存();
        if (true === $r) {
            Session::flash('message', 'tweet した');
        }
        return redirect('/');
    }


なんかifがうざったいから、例外投げるほうが楽かもしんまい。

    public function postTweet(Request $request)
    {
        try {
            $tweetModel = new Tweet();
            $tweetModel->insert用の入力();
            $tweetModel->insert用のvalidate();
            $tweetModel->insert保存();
        } catch(Throwable $e) {
            return redirect('/');
        }

        Session::flash('message', 'tweet した');
        return redirect('/');
    }

こんな感じ。
細かい話をすると「insertとupdate」って、似てるけど処理が違うので。データ取り込みもvalidateも保存する時も、微妙に入り口を分けていたい感じではある。
ので、全体的に「insert用の」ってつけてるの感じ。


Modelのほうはまぁだいたいイメージがつくと思うので適宜オミット。
ちな、これの実装を愚直にやると「formのnameアトリビュート値が固定」になるんだけど。「いやまぁ固定でもいいじゃない」ってのと「可変にしたきゃ、デフォルト引数とかうまく使ってよ」とか、まぁ解決策はいくつか。
面倒なんで書かないけど、質問がきたら書くかも。


あと、細かいところでエラー処理。
まぁ例外投げる時は「例外のメッセージの中に、それこそjson文字列とかででも細かい情報突き返せばよくね?」とかアバウトにアバウトに。いやまぁModelインスタンスん中にエラー情報入れてgetってもいいだろうし。
戻り値でreturnであれば、別途「error_detail」とかってメソッド使って詳細吐き出せばよいと思うですます。


ってなわけで、これやると「Controllerでやることが減る」変わりに「Modelでやることが増える」んだよねぇ当たり前だよ動かしただけだもん。
ただ、Modelって「実のところ、なんなの?」って考えた時に。
もしModelが「ある程度のビジネス処理をつかさどるところ」なのであれば、一つの提案としては上述のようなコード「も」想定できるかなぁ、とか思うわけですます。


一方で「LaravelのController(の1メソッド)って、必ずしも"外部から呼ばれる"前提とは限らない」と思われるので(web.phpとかにルーティング書かなければ呼ばれないし)。
例えば「このControllerのこのメソッドは"このデータの塊を、入力受け取ってvalidateして保存する"用のメソッドなんだ!!」ってなるのであれば、それはそれで「そーゆー方向性と指針なんだなぁ」とも思うのです。「それって、ビジネスロジックって言わね?」とかって疑問もありますが、別にそれはそれで一貫性があれば。「Controllerにビジネスロジックを書いたら死ぬ」ってわけでもないだろうし。
いやまぁ結果的に「Controllerに書く内容が増えて、太りやすくなるよなぁ」とは思うのですが「それはそれでいいじゃん」であれば、それはそれでよいんじゃないかなぁ、とも思いますです。はい。


ってなわけで。
上述みたいなコードで「データに紐づく責務」をModel側に移すのって、世間的にはどーゆー反応なのかしらん? ってのが、おいちゃんの疑問。いや上述のような感じのコードって、見る見ないでいうと「ほとんど見かけない」ので。
「個人的見解と感想と好みと雑感と偏見」ざぶざぶでよいので、コメントなどいただけると、おいちゃんが喜んだりすると思われますので、ぜひ ノ

*1: 「ふとっちょのきょくちょう」(Fat Controller)、って、説明しなきゃわからないギャグを書くな