がるの健忘録

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

Slim docsの解析; Routing

https://www.slimframework.com/docs/v3/objects/router.html
まごうことなき大物。
腰据えていきませう。……下手したら二分割かも。


How to create routes。
うん、ここはまぁOK。
おいちゃん的には

$app = new \Slim\App();
$app->get('/books/{id}', クラス名::clss . ':メソッド名');

しか書かないつもりだけど(ルーティングファイルに処理書くとか以下検閲削除。確認用のちょろりテストは例外)。


あ、一応。
そもそもSlim自体が「PHP5.5+」なのであんまり関係ないんだけど。
class キーワードでクラス名の解決ができる( http://php.net/manual/ja/language.oop5.basic.php )のはPHP 5.5 以降なので、念のために留意。


あとは

$app->any('/books/{id}', クラス名::clss . ':メソッド名');
$app->map(['GET', 'POST'], '/books/{id}', クラス名::clss . ':メソッド名');

辺りの書き方を把握しておけば、かなぁ。

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

の実装見れば、大体ピンとくると思う。
個人的に納得したのが、別のところに書いてあったんだけど

$app->map(['GET', 'POST', 'PUT', 'DELETE'], '/', クラス名::clss . ':メソッド名');

って書き方。あぁ確かに / って「どのメソッドで来てもいい」ってのは、たしかにあるなぁ、と。
まぁこの辺はスタンスと主義主張とお好みで、任意に。


Route callbacks。
RequestとResponseはいいよね。
Argumentsは「3番目の引数は、現在のルートの名前付きプレースホルダの値を含む連想配列です。(機械翻訳)」。
ふむ……ちょいと別件で試したいこともあるので、試してみませう。
まずは単純に「中身の確認」から。


/hoge/gallu

$app = new \Slim\App;
$app->get('/hoge/{name}', function(Request $request, Response $response, array $args) {
    ob_start();
    var_dump($args);
    $s = ob_get_clean();

    $response->getBody()->write($s);
    return $response;
});
array(1) {
  ["name"]=>
  string(5) "gallu"
}

まぁ予想通り。


次。「前方にパラメタ」だと、どうなるんだろ?

$app = new \Slim\App;
$app->get('/{name}/hoge/{age}', function(Request $request, Response $response, array $args) {
    ob_start();
    var_dump($args);
    $s = ob_get_clean();

    $response->getBody()->write($s);
    return $response;
});
array(2) {
  ["name"]=>
  string(5) "gallu"
  ["age"]=>
  string(3) "100"
}

あ、のった。ふむイケルのか。


次、ちょっとひねった確認。

$app = new \Slim\App;
$app->get('/{name}/hoge/{age}', function(Request $request, Response $response, array $args) {
    ob_start();
    var_dump($args);
    $s = ob_get_clean();
    $response->getBody()->write($s);
    return $response;
});
$app->get('/admin/hoge/{age}', function(Request $request, Response $response, array $args) {
    ob_start();
    var_dump($args);
    $s = ob_get_clean();
    $response->getBody()->write($s);
    return $response;
});

http://example.com:8080/admin/hoge/100

array(2) {
  ["name"]=>
  string(5) "admin"
  ["age"]=>
  string(3) "100"
}

ふむ。

$app = new \Slim\App;
$app->get('/admin/hoge/{age}', function(Request $request, Response $response, array $args) {
    ob_start();
    var_dump($args);
    $s = ob_get_clean();
    $response->getBody()->write($s);
    return $response;
});
$app->get('/{name}/hoge/{age}', function(Request $request, Response $response, array $args) {
    ob_start();
    var_dump($args);
    $s = ob_get_clean();
    $response->getBody()->write($s);
    return $response;
});

http://example.com:8080/admin/hoge/100

array(1) {
  ["age"]=>
  string(3) "100"
}

ふむふむ。


とりあえず「先出有効」、と。
ただ、これ、規定の挙動とも限らんだろうしなぁ。ちと、方策等は、しばらく考えておきませう*1


Writing content to the response。
「First, you can simply echo() content from the route callback.」ふへ??? そなの?

$app = new \Slim\App;
$app->get('/', function(Request $request, Response $response, array $args) {
    echo "hoge test\n";
});

あ、ほんとだ動いた。
「Second, you can return a Psr\Http\Message\ResponseInterface object.」は、うん。
とはいえ………$response->write()とかあるし、object returnのほうが、個人的には「よりよい」ように感じるなぁ。
まぁ「ちょっとしたデバッグ用と」で、覚えておきませう。
これだと「response statusとか変えた時」とか、対応が難しそうなケースもあるだろうしね。


Closure binding。
「ルートコールバックとしてClosureインスタンスを使用すると、クロージャの状態はContainerインスタンスにバインドされます。 つまり、$ thisキーワードを使用してClosure内のDIコンテナインスタンスにアクセスできます。(機械翻訳)」。
うん、これはコードでも見てた。
まぁ「Closure渡す」をしない予定なので、あんまり気にならないんだけど。
ちなみに「クラスで動かす」場合は、コンストラクタの引数にContainer instanceがわたってくるざんす。
……今度つくる「おいちゃんskeleton」には、基底クラス、書いておきますかねぇ。


Redirect helper

$app = new \Slim\App();
$app->redirect('/books', '/library', 301);

身もふたもないw
直近お仕事では使わない気がするんだけど、なんかの時に使いそうなので、ほんのりと記憶しておきませう。
「コンテンツの引っ越し込みの改修」とかで使いそうな気がするんだ。


Route strategies
ふむ……「URIに入ってくるパラメタ引数」まわりのお話か。

$c = new \Slim\Container();
$c['foundHandler'] = function() {
    return new \Slim\Handlers\Strategies\RequestResponseArgs();
};

$app = new \Slim\App($c);
$app->get('/hello/{name}', function ($request, $response, $name) {
    return $response->write($name);
});

http://example.com:8080/hello/gallu

gallu


$c = new \Slim\Container();
$c['foundHandler'] = function() {
    return new \Slim\Handlers\Strategies\RequestResponseArgs();
};

$app = new \Slim\App($c);
$app->get('/hello/{name}/{age}', function ($request, $response, $name, $age) {
    return $response->write($name . ':' . $age);
});

http://example.com:8080/hello/gallu/100

gallu:100

と。ふむ。
なんだろうなぁ「好みの問題」な気もするなぁ割と。
……多分、使わない予感w


Route placeholders。
うん散々使ってるw
ただ、この書式は今まで出てこなかったから、ここだけチェックかな。

$app->get('/users[/{id}]', function ($request, $response, $args) {
    // responds to both `/users` and `/users/123`
    // but not to `/users/`
});
$app->get('/news[/{year}[/{month}]]', function ($request, $response, $args) {
    // reponds to `/news`, `/news/2016` and `/news/2016/03`
});

この辺はまぁ「あってもなくても」な記述だね。理解。


ちょいと試したいのが、これ。

$app->get('/news[/{params:.*}]', function ($request, $response, $args) {
    $params = explode('/', $args['params']);

    // $params is an array of all the optional segments
});

ふむ。
実験。

$app = new \Slim\App;
$app->get('/news[/{params:.*}]', function ($request, $response, $args) {
    $params = explode('/', $args['params']);
    var_dump($args, $params);

    // $params is an array of all the optional segments
});

http://example.com:8080/news/

array(1) {
  ["params"]=>
  string(0) ""
}
array(1) {
  [0]=>
  string(0) ""
}

http://example.com:8080/news/hoge

array(1) {
  ["params"]=>
  string(4) "hoge"
}
array(1) {
  [0]=>
  string(4) "hoge"
}

http://example.com:8080/news/hoge/100

array(1) {
  ["params"]=>
  string(8) "hoge/100"
}
array(2) {
  [0]=>
  string(4) "hoge"
  [1]=>
  string(3) "100"
}

あぁ面白い。


Regular expression matching
で、やぱりこれもあるのか。………ってか上述の「.*」って、正規表現か。理解。
なんてのは置いといて、軽くサンプルコード。

$app = new \Slim\App();
$app->get('/users/{id:[0-9]+}', function ($request, $response, $args) {
    // Find user identified by $args['id']
});

どれくらいの正規表現が使えるのかしらん??
そもそも「正規表現あんまり使わない」から不慣れ中の不慣れ、ではあるんだけど。まぁこの辺つかうと、いろいろと便利にできそうでございます。
まぁ不慣れなので、必要になってから調べるとしましょう*2


Route names
あぁうんこれは「リダイレクトする時に名前でリダイレクトさせたい」用途だよねぇそれ以外ってあるのかしらん??

$app = new \Slim\App;
$app->get('/', function(Request $request, Response $response, array $args) {
    echo "hoge test\n";
})->setName('top');
$app->get('/hoge', function(Request $request, Response $response, array $args) {
   echo $this->get('router')->pathFor('top');
});

http://example.com:8080/hoge

/

うん、予想通り。
これ、リダイレクトのメソッドで勝手に解決できたりすると楽そうだなぁ、とか、考えてみたりもする。
実装するときに、ちょっとだけ、考えてみませう。


Route groups
いわゆるグルーピング。

$app = new \Slim\App();
$app->group('/users/{id:[0-9]+}', function () {
    $this->map(['GET', 'DELETE', 'PATCH', 'PUT'], '', function ($request, $response, $args) {
        // Find, delete, patch or replace user identified by $args['id']
    })->setName('user');
    $this->get('/reset-password', function ($request, $response, $args) {
        // Route for /users/{id:[0-9]+}/reset-password
        // Reset the password for user identified by $args['id']
    })->setName('user-password-reset');
});

「グループパターンのプレースホルダ引数は、最終的にネストされたルートで使用できるようになります。(機械翻訳)」あたりが、ポイントっちゃぁポイントかしらん。これが「使えない」だと、いろいろと暴れだすと思うが(笑


あとは、Middlewareあたりを使うのに

$app->group('', function() {
    $this->get('/billing', function ($request, $response, $args) {
        // Route for /billing
    });
    $this->get('/invoice/{id:[0-9]+}', function ($request, $response, $args) {
        // Route for /invoice/{id:[0-9]+}
    });
})->add( new SharedMiddleware() );

なんてのもあるぽいね。


ポイントは「groupsの中では、$appじゃなくて$thisが使われている」あたり。
「Note inside the group closure, $this is used instead of $app. Slim binds the closure to the application instance for you, just like it is the case with route callback binds with container instance.」だって。
この辺はまぁ「ちょろっと気を付けて」いきませう。
まぁ、groupの中で$appつかったら怒られるからすぐに気づくんだろうけど、さ(笑
この辺揃えるなら

$app->group('', function() use($app) {
    $app->get('/billing', function ($request, $response, $args) {
        // Route for /billing
    });
    $app->get('/invoice/{id:[0-9]+}', function ($request, $response, $args) {
        // Route for /invoice/{id:[0-9]+}
    });
});

って書き方を推奨しておいても、よいのかもなぁ、的な。
どっちかなぁ? 悩む。


Route middleware
「You can also attach middleware to any route or route group. Learn more.*3」ほへ。
URIなのか。
したら、別稿で、確認しますか。


Router caching
これも別URI。ただ、設定するのは「ファイル名」だけ、っぽいんだよね。
設定は「routerCacheFile」。
そのうち試してみませう。


Container Resolution
ふむ……
・container_key:method
・Class:method
・An invokable class
・container_key
あるなぁ。まぁ「メソッド名のデフォが__invokable」だったので、三番目はなんとなくわかるんだが。


さて、コード例。

$app->get('/', '\HomeController:home');
$app->get('/', \HomeController::class . ':home');

うん。なんとなし、二番目のほうが「より好み」かなぁ。


で……「container_key」。これ、たぶん「クラス名」をそのままkeyとして入れてるんだよねぇ。
「根拠lessなんだけど微妙に不安の匂いがよぎった」あたりの実装。
まぁ多分、ほとんどの場合では「特に問題ない」んだとおもうので、挙動だけ覚えておきませう。
おそらく「動かすまえに少しインスタンスに小細工したい」時に使える方法なんだろうなぁ、って思う。

$container = $app->getContainer();
$container['HomeController'] = function($c) {
    $view = $c->get("view"); // retrieve the 'view' from the container
    return new HomeController($view);
};

これで

$app->get('/', 'HomeController:home');

って呼べば、いけるはず。
まぁこの辺も「実際に必要になった」ら、検証してみませう。


Allow Slim to instantiate the controller
「Alternatively, if the class does not have an entry in the container, then Slim will pass the container’s instance to the constructor. You can construct controllers with many actions instead of an invokable class which only handles one action.」うんコード読んだ。
だから

class BaseController
{
   protected $container;
   // constructor receives container instance
   public function __construct(ContainerInterface $container) {
       $this->container = $container;
   }
}

ってのを作ると扱いやすいんだろうなぁ、って思ってる。


Using an invokable class
については、ど〜すっかなぁ、程度。

$app->get('/', \HomeAction::class);

でいけるのはメリットがあるように思われるので、「1Controllerクラスあたり1Action」でよいなら、これもありなのかもなぁ、とは思う、など。


思ったより常識的な長さで解析が終わりました………Middlewareが別URIだったしね(笑

*1:後述しますが、正規表現使うのが、多分、一番確実

*2:なんてことやってるからいつまでも不慣れなのだが

*3:https://www.slimframework.com/docs/v3/concepts/middleware.html

Slim docsの解析; The Response

https://www.slimframework.com/docs/v3/objects/response.html
ここも結構な大物だろう、と予想。


How to get the Response object
いやまぁHow to言われても「呼ばれる関数(メソッド)の第二引数でゲトれる」くらいだしなぁ。

<?php
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

$app = new \Slim\App;
$app->get('/foo', function (ServerRequestInterface $request, ResponseInterface $response) {
    // Use the PSR 7 $response object

    return $response;
});
$app->run();

ここは一応突っ込みが無くもなくて。
「文字列をreturnすると、現状の$responseにその文字列をbodyとして書き込むような処理を追記してくれる」んだよね。
http://d.hatena.ne.jp/gallu/20180613/p1

        } 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したらよしなにしてくれる」は、期待してよい動作、なんじゃないかなぁ? と思ってる。

The PSR 7 response object is injected into your Slim application middleware as the second argument of the middleware callable like this:

<?php
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

$app = new \Slim\App;
$app->add(function (ServerRequestInterface $request, ResponseInterface $response, callable $next) {
    // Use the PSR 7 $response object

    return $next($request, $response);
});
// Define app routes...
$app->run();

middleware周りはまた今度、まとめて徹底的に。なので、今回は一旦、パス。


The Response Status。割と大事ぽ。

$newResponse = $response->withStatus(302);

ここだけ把握しておけばよいんじゃないかなぁ………あぁ、一点気になること。
Response(以外もいくつかそうだけど)は、いわゆる「不変インスタンスである」とされているから、withStatusの実装って


vendor/slim/slim/Slim/Http/Response.php

    public function withStatus($code, $reasonPhrase = '')
    {
        $code = $this->filterStatus($code);

        if (!is_string($reasonPhrase) && !method_exists($reasonPhrase, '__toString')) {
            throw new InvalidArgumentException('ReasonPhrase must be a string');
        }

        $clone = clone $this;
        $clone->status = $code;
        if ($reasonPhrase === '' && isset(static::$messages[$code])) {
            $reasonPhrase = static::$messages[$code];
        }

        if ($reasonPhrase === '') {
            throw new InvalidArgumentException('ReasonPhrase must be supplied for this code');
        }

        $clone->reasonPhrase = $reasonPhrase;

        return $clone;
    }

なんだよね。ポイントは「$clone = clone $this;」して「return $clone;」ってあたり。
なので、この辺でさっきの「文字列をreturn」と組み合わせると、なんか不一致が出そうな気が少しする、ので、気を付けたほうがよいかも。
………うん、returnは「ちゃんとResponseインスタンス返す」ようにしたほうが安全かも。
ちと、確認してみませう。


public/index.php

$app = new \Slim\App;
$app->get('/add_header', function(Request $request, Response $response, array $args) {
    $response = $response->withStatus(304);
    //return 'test';
    return $response->getBody()->write('test');
});

$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /add_header HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Host: example.com
Date: Mon, 18 Jun 2018 05:07:09 +0000
Connection: close
X-Powered-By: PHP/7.2.6
Content-Type: text/html; charset=UTF-8
Content-Length: 4

testConnection closed by foreign host.

………あれ?
…………あ。
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;
    }

戻り値、stringか。


public/index.php

$app->get('/add_header', function(Request $request, Response $response, array $args) {
    $response = $response->withStatus(304);
    $response->getBody()->write('test');
    //return 'test';
    return $response;
});

$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /add_header HTTP/1.1
Host: example.com

HTTP/1.1 304 Not Modified
Host: example.com
Date: Mon, 18 Jun 2018 05:18:11 +0000
Connection: close
X-Powered-By: PHP/7.2.6

Connection closed by foreign host.

よし意図通り。
ってことは

$app->get('/add_header', function(Request $request, Response $response, array $args) {
    $response = $response->withStatus(304);
    //$response->getBody()->write('test');
    return 'test';
});

$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /add_header HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Host: example.com
Date: Mon, 18 Jun 2018 05:19:06 +0000
Connection: close
X-Powered-By: PHP/7.2.6
Content-Type: text/html; charset=UTF-8
Content-Length: 4

testConnection closed by foreign host.

だ〜よ〜ね〜〜。
これ、お作法的に
「最後は return $response; 」っての、徹底したほうが安全かも。ここだけは「Response、不変インスタンスじゃなければよかったのに」とか思う瞬間。まぁ、Slimがそう定義した訳でもない、から、ねぇ。
(実はお便利メソッドがresponseに生えてます。後述するwriteメソッド)。


The Response Headers。

$headers = $response->getHeaders();

とか、むしろ「デバッグの文脈」とか「解析の文脈」とか「テストの文脈」とか、で、便利そうな気がする。
便利ではあると思われるので、ほんのりと記憶しておきませう。


まぁあとは

$headerValueArray = $response->getHeader('Vary');
$headerValueString = $response->getHeaderLine('Vary');
if ($response->hasHeader('Vary')) {

とかあって

$newResponse = $oldResponse->withHeader('Content-type', 'application/json');
$newResponse = $oldResponse->withAddedHeader('Allow', 'PUT');

とかあるので、よき……なんだが、2点。
一つは「戻り値がインスタンスである」って点。「不変インスタンスだったよねぇ」ってのを忘れると色々と切ない気がするので、注意。
もう一つは「withHeaderとwithAddedHeaderの違い」。
どちらも vendor/slim/slim/Slim/Http/Message.php に実装があって、かつ

    public function withAddedHeader($name, $value)
    {
        $clone = clone $this;
        $clone->headers->add($name, $value);

        return $clone;
    }

とかって実装なので、本当の実装は
vendor/slim/slim/Slim/Http/Headers.php

    public function add($key, $value)
    {
        $oldValues = $this->get($key, []);
        $newValues = is_array($value) ? $value : [$value];
        $this->set($key, array_merge($oldValues, array_values($newValues)));
    }
    public function set($key, $value)
    {
        if (!is_array($value)) {
            $value = [$value];
        }
        parent::set($this->normalizeKey($key), [
            'value' => $value,
            'originalKey' => $key
        ]);
    }

この辺。
「複数行」って割と(業務で付与するheader的には)珍しいと思うので、「基本は withHeader() 」って覚えておいて、そんなに問題ないんじゃないかなぁ。


あぁもちろん
Remove Header

$newResponse = $oldResponse->withoutHeader('Allow');

もそろってるので、適宜よしなに。
これも不変インスタンス以下略。


The Response Body については。

$body = $response->getBody();

でとれる……のは、bodyインスタンス。デフォは vendor/slim/slim/Slim/Http/Body.php ここ。
なので

$response->getBody()->write();

この辺が、まぁよく使うところかなぁ、と。
上にも書いたけど「これの戻り値、string」なので、色々と注意、ではある。


Returning JSON
………あら、ナウい感じ。
vendor/slim/slim/Slim/Http/Response.php

    public function withJson($data, $status = null, $encodingOptions = 0)
    {
        $response = $this->withBody(new Body(fopen('php://temp', 'r+')));
        $response->body->write($json = json_encode($data, $encodingOptions));

        // Ensure that the json encoding passed successfully
        if ($json === false) {
            throw new \RuntimeException(json_last_error_msg(), json_last_error());
        }

        $responseWithJson = $response->withHeader('Content-Type', 'application/json;charset=utf-8');
        if (isset($status)) {
            return $responseWithJson->withStatus($status);
        }
        return $responseWithJson;
    }

便利〜。
………こっちは「return $response->withJson($data);」ってやってもいいのか………微妙に面倒w
まぁ「withついてるからcloneインスタンスが返ってくる」って覚えてもいいんだけどねぇ。
とはいえいずれにしても、json returnが考慮されているのは、よいこった。


……で、何気なくみていたら
vendor/slim/slim/Slim/Http/Response.php

    public function write($data)
    {
        $this->getBody()->write($data);

        return $this;
    }

やだぁ便利なのあるじゃないですか。
ってことは

return $response->write(HTML文字列);

でいけますなぁ。
こっちこっち。使うの、こっち。


Returning a Redirect。
うん、これも大事。

return $response->withRedirect('/new-url', 301);

シンプル。
………Slimって、ルーティングに「名前付き」ってできたんだっけか?
https://www.slimframework.com/docs/v3/objects/router.html#route-names
に出てくるっぽいので、次回に回そうw


いじょ。
割とシンプルだったなぁ………そういや、Cookie周りのことはなんも書いてないw
まぁ、一通り掘り込んだ上で「なかったら」改めて考察しませう。

Slim docsの解析; The Request

https://www.slimframework.com/docs/v3/objects/request.html
多分、ここは大物w


とりあえずルーティング設定。
関数とか「設定できる」んだろうけどやる気はないんで、無視w


The Request Method
………まぁ一通り。GET,PUT,POST,DELETEくらいしか使う気ないしなぁ。


で、メソッド指定2種。

You can include a _METHOD parameter in a POST request’s body. The HTTP request must use the application/x-www-form-urlencoded content type.

POST /path HTTP/1.1
Host: example.com
Content-type: application/x-www-form-urlencoded
Content-length: 22

data=value&_METHOD=PUT

You can also override the HTTP request method with a custom X-Http-Method-Override HTTP request header. This works with any HTTP request content type.

POST /path HTTP/1.1
Host: example.com
Content-type: application/json
Content-length: 16
X-Http-Method-Override: PUT

{"data":"value"}


ふむ……この辺、お他所はどうだっただろさね??
ちぃと実験コード書いてみるかねぇ。
実験目的としては
・そもそも、ちゃんと「PUT」とかのメソッドでcallした時はどーゆールーティングになるのか
・上述のような「メソッドと上書きメソッド」の時の動き
あたり。


まずは、壮絶に雑なコードを一筆。
public/index.php

$app = new \Slim\App;
$app->get('/', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, get\n");
    return $response;
});
$app->post('/', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, post\n");
    return $response;
});
$app->put('/', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, put\n");
    return $response;
});
$app->delete('/', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, delete\n");
    return $response;
});


んで、おおざっぱにcall。

$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Host: example.com
Date: Fri, 15 Jun 2018 08:14:49 +0000
Connection: close
X-Powered-By: PHP/7.2.6
Content-Type: text/html; charset=UTF-8
Content-Length: 11

Hello, get
Connection closed by foreign host.

よし。


では、PUTで試し。

$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
PUT / HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Host: example.com
Date: Fri, 15 Jun 2018 08:15:49 +0000
Connection: close
X-Powered-By: PHP/7.2.6
Content-Type: text/html; charset=UTF-8
Content-Length: 11

Hello, put
Connection closed by foreign host.

ふむりやっぱり無問題。
一応、DELETEも。

$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
DELETE / HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Host: example.com
Date: Fri, 15 Jun 2018 08:16:16 +0000
Connection: close
X-Powered-By: PHP/7.2.6
Content-Type: text/html; charset=UTF-8
Content-Length: 14

Hello, delete
Connection closed by foreign host.

OK。


で、これに「糅てて加えて」別フォーマットがあるのか。
ちと修正して。

$app = new \Slim\App;
$app->get('/path', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, get\n" . 'getMethod: ' . $request->getMethod() . "\n" . 'getOriginalMethod: ' . $request->getOriginalMethod() . "\n");
    return $response;
});
$app->post('/path', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, post\n" . 'getMethod: ' . $request->getMethod() . "\n" . 'getOriginalMethod: ' . $request->getOriginalMethod() . "\n");
    return $response;
});
$app->put('/path', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, put\n" . 'getMethod: ' . $request->getMethod() . "\n" . 'getOriginalMethod: ' . $request->getOriginalMethod() . "\n");
    return $response;
});
$app->delete('/path', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, delete\n" . 'getMethod: ' . $request->getMethod() . "\n" . 'getOriginalMethod: ' . $request->getOriginalMethod() . "\n");
    return $response;
});


サンプルに出ているのを、そのまま。

$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
POST /path HTTP/1.1
Host: example.com
Content-type: application/x-www-form-urlencoded
Content-length: 22

data=value&_METHOD=PUT
HTTP/1.1 200 OK
Host: example.com
Date: Fri, 15 Jun 2018 08:20:52 +0000
Connection: close
X-Powered-By: PHP/7.2.6
Content-Type: text/html; charset=UTF-8
Content-Length: 50

Hello, put
getMethod: PUT
getOriginalMethod: POST
Connection closed by foreign host.

$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
POST /path HTTP/1.1
Host: example.com
Content-type: application/json
Content-length: 16
X-Http-Method-Override: PUT

{"data":"value"}
HTTP/1.1 200 OK
Host: example.com
Date: Fri, 15 Jun 2018 08:21:29 +0000
Connection: close
X-Powered-By: PHP/7.2.6
Content-Type: text/html; charset=UTF-8
Content-Length: 50

Hello, put
getMethod: PUT
getOriginalMethod: POST
Connection closed by foreign host.

ふむ面白い。
個人的にはX-Http-Method-Overrideヘッダのほうが好きかなぁ、なんとなく。


………ちと、好奇心。

$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
POST /path HTTP/1.1
Host: example.com
Content-type: application/x-www-form-urlencoded
Content-length: 22
X-Http-Method-Override: DELETE

data=value&_METHOD=PUT
HTTP/1.1 200 OK
Host: example.com
Date: Fri, 15 Jun 2018 08:23:29 +0000
Connection: close
X-Powered-By: PHP/7.2.6
Content-Type: text/html; charset=UTF-8
Content-Length: 56

Hello, delete
getMethod: DELETE
getOriginalMethod: POST
Connection closed by foreign host.

ヘッダのほうが強いのかしらん?
まぁあんまりない事象だとは思うんだけど、後々用に、軽く実験。


お次はThe Request URI

The PSR 7 Request object’s URI is itself an object that provides the following methods to inspect the HTTP request’s URL parts:

    getScheme()
    getAuthority()
    getUserInfo()
    getHost()
    getPort()
    getPath()
    getBasePath()
    getQuery() (returns the full query string, e.g. a=1&b=2)
    getFragment()
    getBaseUrl()

へぇ。色々あるのか。

You can get the query parameters as an associative array on the Request object using getQueryParams().

これは多分使わないかなぁ。

Base Path
If your Slim application's front-controller lives in a physical subdirectory beneath your document root directory, you can fetch the HTTP request's physical base path (relative to the document root) with the Uri object's getBasePath() method. This will be an empty string if the Slim application is installed in the document root's top-most directory.

あ、解析で出てきたBase Path。
「Slimアプリケーションのフロントコントローラがドキュメントルートディレクトリの下の物理サブディレクトリにある場合、UriオブジェクトのgetBasePath()メソッドを使用して、HTTPリクエストの物理ベースパス(ドキュメントルートからの相対パス)を取得できます。(機械翻訳)」なるほどぉ。
Cookieとかの設定用途とか、で、使いそうだなぁ。
ちと、どこかで必要な時にでも、がっつりやってみよう。


The Request Headers
うんまぁ。

$headers = $request->getHeaders();
foreach ($headers as $name => $values) {
    echo $name . ": " . implode(", ", $values);
}

で、とれるぽ。
あとは

$headerValueArray = $request->getHeader('Accept');
$headerValueString = $request->getHeaderLine('Accept');
if ($request->hasHeader('Accept')) {
    // Do something
}

など。


The Request Body
あ、ちょうど気になってたやつ。

$parsedBody = $request->getParsedBody();


public/index.php

$app->put('/path', function(Request $request, Response $response, array $args) {
    ob_start();
    var_dump( $request->getParsedBody() );
    $s = ob_get_clean();

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

$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
PUT /path HTTP/1.1
Host: example.com
Content-type: application/x-www-form-urlencoded
Content-length:21

data=value&data2=test
HTTP/1.1 200 OK
Host: example.com
Date: Fri, 15 Jun 2018 08:45:17 +0000
Connection: close
X-Powered-By: PHP/7.2.6
Content-Type: text/html; charset=UTF-8
Content-Length: 91

Hello, put
array(2) {
["data"]=>
string(5) "value"
["data2"]=>
string(4) "test"
}

Connection closed by foreign host.

$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
PUT /path HTTP/1.1
Host: example.com
Content-type: application/json
Content-length:31

{"data":"value", "data2": 10}
HTTP/1.1 200 OK
Host: example.com
Date: Fri, 15 Jun 2018 08:44:25 +0000
Connection: close
X-Powered-By: PHP/7.2.6
Content-Type: text/html; charset=UTF-8
Content-Length: 82

Hello, put
array(2) {
["data"]=>
string(5) "value"
["data2"]=>
int(10)
}

Connection closed by foreign host.

こっちは綺麗に。


………ふむ。

$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
PUT /path HTTP/1.1
Host: example.com
Content-length:31

{"data":"value", "data2": 10}
HTTP/1.1 200 OK
Host: example.com
Date: Fri, 15 Jun 2018 08:46:09 +0000
Connection: close
X-Powered-By: PHP/7.2.6
Content-Type: text/html; charset=UTF-8
Content-Length: 17

Hello, put
NULL

Connection closed by foreign host.

あぁ。Content-type見てるのか。うん、綺麗だ。


さて………

$body = $request->getBody();

ってのもあるが……なんかこってりしそうだなぁ。

$app->put('/path', function(Request $request, Response $response, array $args) {
    ob_start();
    //var_dump( $request->getParsedBody() );
    var_dump( $request->getBody() );
    $s = ob_get_clean();

    $response->getBody()->write("Hello, put\n" . $s . "\n");
    return $response;
});
$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
PUT /path HTTP/1.1
Host: example.com
Content-type: application/json
Content-length:31

{"data":"value", "data2": 10}
HTTP/1.1 200 OK
Host: example.com
Date: Fri, 15 Jun 2018 08:47:56 +0000
Connection: close
X-Powered-By: PHP/7.2.6
Content-Type: text/html; charset=UTF-8
Content-Length: 304

Hello, put
object(Slim\Http\RequestBody)#50 (7) {
  ["stream":protected]=>
  resource(46) of type (stream)
  ["meta":protected]=>
  NULL
  ["readable":protected]=>
  NULL
  ["writable":protected]=>
  NULL
  ["seekable":protected]=>
  NULL
  ["size":protected]=>
  NULL
  ["isPipe":protected]=>
  NULL
}

Connection closed by foreign host.

あ。思ったより穏当。
細かいところはまた今度、実装する時に見てみましょ。


同様に後回ししたいのがUploaded Files。

$files = $request->getUploadedFiles();

で「Each object in the $files array is a instance of \Psr\Http\Message\UploadedFileInterface 以下略」なので、「1ファイルが1インスタンス」って単位だろうから、さほど困ることもなさそうだ。


お次、Request Helpers。
いわゆる「お便利さん」ぽ。


Detect XHR requests………XMLHttpRequestとか、久しく耳にも目にもしておりませぬなぁ。割愛w


Content Type………まぁ欲しい、時も。

$contentType = $request->getContentType();


Media Type………お外から入ってくるこれは全く信用してないw、ので、割愛。


Character Set、Content Length。うんまぁ欲しい時はほしいだろうさね。

$charset = $request->getContentCharset();
$length = $request->getContentLength();


Request Parameter。

To fetch single request parameter value, use methods: getParam(), getQueryParam(), getParsedBodyParam(), getCookieParam(), getServerParam(), counterparts of PSR-7’s plural form get*Params() methods.

「単体の値取得」ならこれ、ですか。入力で一番使いそうな予感。
ざっくりいくんなら getParam() かしらん? とも思うんだが、ざっくり過ぎる気もする。

    public function getParam($key, $default = null)
    {
        $postParams = $this->getParsedBody();
        $getParams = $this->getQueryParams();
        $result = $default;
        if (is_array($postParams) && isset($postParams[$key])) {
            $result = $postParams[$key];
        } elseif (is_object($postParams) && property_exists($postParams, $key)) {
            $result = $postParams->$key;
        } elseif (isset($getParams[$key])) {
            $result = $getParams[$key];
        }

        return $result;
    }

あぁ「$_POSTまたは$_GET」的な感じ、なのか。かつ、$_POST的なほうが優先度高め。
これなら、あり、かなぁ。


Route Object

Sometimes in middleware you require the parameter of your route.

む……middlewareのお作法をそもそも理解していない。
add()、が、そうなのかし??
あとで、具体的には「Routing」のところで #route-middleware で項目があるから、その時にさかのぼってみませうか。


Media Type Parsers
ん……「中の動き」っぽいなぁ。一旦省略。


Attributes
あ、よぉ分からん所だった一つ。

$app->add(function ($request, $response, $next) {
    $request = $request->withAttribute('session', $_SESSION); //add the session storage to your request as [READ-ONLY]
    return $next($request, $response);
});
$app->get('/test', function ($request, $response, $args) {
    $session = $request->getAttribute('session'); //get the session from the request

    return $response->write('Yay, ' . $session['name']);
});

あぁ。
MagicWeaponだと「bag」とか言ってるあたりのやつか。「持ちまわる諸々」。
requestは「不変インスタンス」だから、withAttributeは「内部でcloneしている」んだよね、的な。なので「戻り値を受け取って、そっちを$nextに渡してる」。
この辺もmiddlewareかな。


……そういえば、セッション的なのって、どうなってるんだべさね?
その辺も少し意識に入れておきませう。


ってなわけでRequest終了。
やっぱり長かったw

Slim docsの解析; The Application

https://www.slimframework.com/docs/
の中から「The Application」をざっくりと見て、興味深いところをメモ。


https://www.slimframework.com/docs/v3/objects/application.html

$config = [
    'settings' => [
        'displayErrorDetails' => true,

        'logger' => [
            'name' => 'slim-app',
            'level' => Monolog\Logger::DEBUG,
            'path' => __DIR__ . '/../logs/app.log',
        ],
    ],
];
$app = new \Slim\App($config);

settingはスケルトンで見たので知ってる〜。


ただ
Updating Settings

$settings = $container->get('settings');
$settings->replace([
        'displayErrorDetails' => true,
        'determineRouteBeforeAppMiddleware' => true,
        'debug' => true
    ]);
]);

は、なるほどふむり、興味深い。
……ただ、ってことは「Slimのデフォの値を把握してなきゃなんない」よねぇって思ったら Slim Default Settings がある、ので、ざっくりと備忘録的に。


httpVersion
まぁそのまんまだよねぇ。デフォ1.1。いつか2.0になるのかねぇ???


responseChunkSize
いわゆる「送りつける1塊のサイズ」。この辺、あんまりPHPだと意識していないような気がするんだけど、意識するとなにか変わるのかしらん?
デフォは4096バイト。


outputBuffering
出力バッファリングの有無。
fasleで「バッファリングしない」、appendで「後ろにつける」、prependで「前につける」、のいずれか。……それ以外の値の時ってどうなるんだろうねぇ?
デフォはappend。まぁデフォでいいと思うなぁ。
単純なtrue(出力バッファリングだけする)とか欲しいような気もしたけど、それ、appendになるわ。


determineRouteBeforeAppMiddleware
長いw
「trueの場合、ミドルウェアが実行される前にルートが計算されます。(機械翻訳)」………ふむ、なんかグっと来ない感じ。
デフォはfalseなので、あんまり使わないかしらん?


displayErrorDetails
「trueの場合、例外に関する追加情報がデフォルトのエラーハンドラによって表示されます。(機械翻訳)」で、デフォがfalse。
ふむ……開発環境でtrueにしておくと旨味があるのかしらん?


addContentLengthHeader
Content-Length付与の有無。デフォはtrueだし、まぁここはtrueのままでいいんじゃないかなぁ?


routerCacheFile
ルートファイルのキャッシュの有無で、デフォルトはfalse(キャッシュしない)。
……キャッシュの仕組みがよっぽどわかればともかく、それ以外だと、あんまりグっと来ないかなぁ。


設定したくなりそうなのはdisplayErrorDetailsくらいかしらん?
………あれ?
上にある「debug」ってなんじゃらほい?
grepしてみたけど、グっとくる情報がないなぁ。slim_test_plain(Slimしか入れてない環境)だと、引っかかってこないし。
とりあえず「一旦、デフォでは存在しない」って認識でOKかしらん。


あと。
$settings->replace()、スケルトンのほうだと使ってないのだよねぇ……軽く確認。

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

ふむ、array_mergeがポイントか。
array_mergeは後だし有効だったはずだから………userSettingsが上書き。
なので「明示的にコンストラクタに渡して設定したらそっちが有効になる」なぁ。
ってことは、index.php付近での「初期設定」の時は「値を渡しておけば問答無用でそっち」になるぽ。
$settings->replace()は、とりあえず「使わない」かなぁ。この状態だと。


割とシンプルに調査終了。

Slim skeletonの解析

さて続きましては slim/slim-skeleton の解析。
https://github.com/slimphp/Slim-Skeleton なので、なんとなし「多分公式が推奨してるんじゃないかと思われる方向性」が確認できるんじゃないかなぁ、と期待。
……で調べたら、別の人が作ったスケルトンもあるのね( https://github.com/oanhnn/slim-skeleton とか)。
ふぅんこれはこれで面白いなぁ、と思ったんだが、まぁいったんは「公式の推奨方向」の確認から。


まずは、起点から確認をしてみませう。
public/index.php

if (PHP_SAPI == 'cli-server') {
    // To help the built-in PHP dev server, check if the request was actually for
    // something which should probably be served as a static file
    $url  = parse_url($_SERVER['REQUEST_URI']);
    $file = __DIR__ . $url['path'];
    if (is_file($file)) {
        return false;
    }
}

require __DIR__ . '/../vendor/autoload.php';

session_start();

// Instantiate the app
$settings = require __DIR__ . '/../src/settings.php';
$app = new \Slim\App($settings);

// Set up dependencies
require __DIR__ . '/../src/dependencies.php';

// Register middleware
require __DIR__ . '/../src/middleware.php';

// Register routes
require __DIR__ . '/../src/routes.php';

// Run app
$app->run();

前回は(一応公式に書かれたものをベースにしてるとはいえ)手で作ったのが、今回はあらかじめ入っているという素晴らしさ。
さて、細かく見ていきませう。

if (PHP_SAPI == 'cli-server') {
    // To help the built-in PHP dev server, check if the request was actually for
    // something which should probably be served as a static file
    $url  = parse_url($_SERVER['REQUEST_URI']);
    $file = __DIR__ . $url['path'];
    if (is_file($file)) {
        return false;
    }
}

へぇブロックしたいんだ。
「なぜブロックしたいのか」が微妙に不明なんだけど、まぁいったん。
「ビルトインのPHP devサーバを手助けするために、リクエストが実際には静的ファイルとして提供されるべきものであるかどうかを確認してください(機械翻訳)」って書いてあるので、まぁ、なんか 必要なんでしょう、程度にざっくりと。
いや「is_fileのif文の中がfalse returnなのが不思議だなぁ」程度の感触。

require __DIR__ . '/../vendor/autoload.php';

いわゆる「一般的なオートローダーの取り込み」。これは前述の時もあったのであんまり気にならず。

session_start();

へぇセッション使うようにしてるのか。
……うん確かに、plainなのだとセッション使ってなかったような記憶がかすかにあるなぁ。
でもこれ「セッション保存をテーブルにしたいとき」の処理とか、どうしよ?
プレファイル作る? おとなしくindex.phpに直書き?

// Instantiate the app
$settings = require __DIR__ . '/../src/settings.php';
$app = new \Slim\App($settings);

Appに引数渡してる。
settings.phpって「配列を返す」んだよね。


src/settings.php

<?php
return [
    'settings' => [
        'displayErrorDetails' => true, // set to false in production
        'addContentLengthHeader' => false, // Allow the web server to send the content-length header

        // Renderer settings
        'renderer' => [
            'template_path' => __DIR__ . '/../templates/',
        ],

        // Monolog settings
        'logger' => [
            'name' => 'slim-app',
            'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log',
            'level' => \Monolog\Logger::DEBUG,
        ],
    ],
];

初手のkey「setting」は決まり文句かしらん。変えてもいいけど後々面倒だし、変えないほうが楽だよねぇ、的な。
…… config() 関数とか作って楽をしてもよいかも。
あと、自動でMonolog入れてる。
displayErrorDetails と addContentLengthHeader は「Slimがもともと使うことを想定していた変数」だねぇ。……今度その辺も整理してみようかしらん?


まぁいずれにしても「いろいろな設定情報を取り込んでる」のが見て取れるげ。
「環境差異のある情報」とかも、うまいことここに入れ込みたいなぁ。


次。

// Set up dependencies
require __DIR__ . '/../src/dependencies.php';


src/dependencies.php

$container = $app->getContainer();

// view renderer
$container['renderer'] = function ($c) {
    $settings = $c->get('settings')['renderer'];
    return new Slim\Views\PhpRenderer($settings['template_path']);
};

// monolog
$container['logger'] = function ($c) {
    $settings = $c->get('settings')['logger'];
    $logger = new Monolog\Logger($settings['name']);
    $logger->pushProcessor(new Monolog\Processor\UidProcessor());
    $logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
    return $logger;
};

containerに入れるやつだ。
PhpRendererって知らないなぁ……Zend2に入ってるやつかな??
この辺は差し替えたいかも。まぁ差し替えるのは極めて簡単でせう。

// Register middleware
require __DIR__ . '/../src/middleware.php';

src/middleware.php

<?php
// Application middleware

// e.g: $app->add(new \Slim\Csrf\Guard);

多分ってか確実に「ミドルウェアの設定を書くところ」。これが「実処理を書くところ」だったらびっくりだお(笑
ミドルウェアは、まだ、全然触ってないからなぁ。
ミドルウェア自体をちゃんと扱い始めてから、改めて考えてみませう。

// Register routes
require __DIR__ . '/../src/routes.php';

src/routes.php

use Slim\Http\Request;
use Slim\Http\Response;

// Routes

$app->get('/[{name}]', function (Request $request, Response $response, array $args) {
    // Sample log message
    $this->logger->info("Slim-Skeleton '/' route");

    // Render index view
    return $this->renderer->render($response, 'index.phtml', $args);
});

ルーティング書くところ。
まぁそのまんま、だねぇ。
戻り値が、おそらくはこれ「string型」のreturnだなぁ。まぁわかりやすくてよいと思う。


で、あとは

// Run app
$app->run();

は同じ。


あとはphpunit.xmlが入っていて tests/Functional があるあたりが興味深いかな。
ちと、軽く潜ってみませう。


まずはphpunit.xml

<phpunit bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="SlimSkeleton">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

うんまぁなんていうか「これ以上ないってくらいスタンダード」なパターンだと思うんだがどうだろw
testsディレクトリって、大体「この名前」だよねぇ。それ以外のってあんまり見たことがない。
さて。testsん中にはFunctionalってディレクトリがあって。
BaseTestCase.phpが「継承元クラス」っぽいので、先に片付きそうなHomepageTest.phpを軽く触ってからBaseTestCase.phpにいきませう。


HomepageTest.php

    public function testGetHomepageWithoutName()
    {
        $response = $this->runApp('GET', '/');

        $this->assertEquals(200, $response->getStatusCode());
        $this->assertContains('SlimFramework', (string)$response->getBody());
        $this->assertNotContains('Hello', (string)$response->getBody());
    }

メソッドの一例。
「testから始まっているよねぇ」とかいうベーシックなところはおいといて。
「$this->runApp」とかが多分「このメソッドでこのURIを叩いてみる」的な動きなんだねぇ。………へぇ便利だ。
assertContainsとかあんまり使わないから記憶にないなぁ………なるほど「この文字が含まれる」か。
assertNotContainsは逆で「含まれないこと」と。
とりあえず「$this->assertEquals(200, $response->getStatusCode());」を全Pageぶん回すだけでも、色々と楽なんじゃなかろうか? 認証系とかあるんだろうけどさ。


では、速やかにBaseTestCase.php
直近はrunApp()と、あとは初期処理系のメソッドの確認かなぁ。


BaseTestCase.php

class BaseTestCase extends \PHPUnit_Framework_TestCase
{

「だ〜よ〜ね〜〜」なあたりの確認からstart。


………ふむ、__constructはなし、か。
PHPUnit、たしか

setUpBeforeClass() // 一回だけ
loop {
	setUp()
		テストメソッド本体
	tearDown()
} pool
tearDownAfterClass() // 一回だけ

こーゆー構造だったよねぇ。


setUpBeforeClass()は……なし。
setUp()……も、なし、か。したら直接runApp()いきますか。
……っつかこのクラス、runApp()のみだわ。

    public function runApp($requestMethod, $requestUri, $requestData = null)
    {
        // Create a mock environment for testing with
        $environment = Environment::mock(
            [
                'REQUEST_METHOD' => $requestMethod,
                'REQUEST_URI' => $requestUri
            ]
        );

引数は一旦、おいといて。
Environment::mock、ですか。


vendor/slim/slim/Slim/Http/Environment.php を見てみる……あぁCollectionの継承先クラス、なのか。
ようは「データをためておく」程度の感じ。
んで

        // Set up a request object based on the environment
        $request = Request::createFromEnvironment($environment);

か。

    public static function createFromEnvironment(Environment $environment)
    {

Environmentが引数の前提だし、完全に「これ専用のメソッド」だねぇ。

        // Set up a response object
        $response = new Response();

これは、まぁ、うん。

        // Use the application settings
        $settings = require __DIR__ . '/../../src/settings.php';

        // Instantiate the application
        $app = new App($settings);

        // Set up dependencies
        require __DIR__ . '/../../src/dependencies.php';

        // Register middleware
        if ($this->withMiddleware) {
            require __DIR__ . '/../../src/middleware.php';
        }

        // Register routes
        require __DIR__ . '/../../src/routes.php';

この辺も、まぁ、うん。
ただ、ここ「共通化」できねぇかなぁ?

        // Process the application
        $response = $app->process($request, $response);

process()………あぁrun()で呼ばれる所の「実処理担当」のあたりだ。ふむふむ。
んで

        // Return the response
        return $response;

か、なるほど。


たしかに「URIを渡して、実行部分だけ切り抜いてる」感じだなぁ。
これなら、多分
・Content-typeがjsonかどうか
jsonのフォーマットの確認
とかもできそうな気がするので、ざっくりしたテストくらいなら、一通りかけそうだ。


なお、tearDown()もtearDownAfterClass()も見つかりませんでしたまる。


細かい話をすると「なんで Functional ?」とか思うんだが、多分これはおいちゃんの英語能力の欠落が原因だろうなぁ、的な。
………あぁ、そうか。例えばCodeceptionなんかでも「機能テスト」ってあるし、URI見ると FunctionalTests だから、これでよいのか。
そうすると……同じレベルで「Unit」とか作るべきかなぁ。納品的には「Acceptance」も大事だろうし。
まぁ、その辺もおいおい、だねぇ。


ふむ………
なんとなく「もう少しいろいろと付け足したりしたい」感じではあるけど、確かに、ベースにはできそうな気がする。
その辺の「付け足したいところ」は、実際に書きながら、少しづつ考えていきませうか。

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

解析その3

そろそろ核心………かなぁ?w


前回出てきた、本命くさい
vendor/slim/slim/Slim/App.php
の__invoke()メソッド。

        // Get the route info
        $routeInfo = $request->getAttribute('routeInfo');

なんとない想像はできるんだけど、躊躇なくvar_dumpして確認w

NULL

………うを?
ちと先に、斜めにgetAttribute()を確認してみませう。


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

    public function getAttributes()
    {
        return $this->attributes->all();
    }
    protected $attributes;
    public function __construct(
 -中略-
        $this->attributes = new Collection();

ふむ………

    public function withAttribute($name, $value)
    {
        $clone = clone $this;
        $clone->attributes->set($name, $value);

        return $clone;
    }

とかはあるんだけど、大本に対して、いわゆる「setAttribute」的なのはないっぽいなぁ。
なんだろ?
まぁいいや。


あらためて
vendor/slim/slim/Slim/App.php

        // 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はどうなるんだろ?

array(4) {
[0]=>
int(1)
[1]=>
string(6) "route0"
[2]=>
array(1) {
["name"]=>
string(4) "gallu"
}
["request"]=>
array(2) {
[0]=>
string(3) "GET"
[1]=>
string(35) "http://dev.example.com:8080/hello/gallu"
}
}

あぁこっちで予想通りのが入ってきた。
………なんとなく「URIにパラメタがあるから」??
ちと実験してみよう。


public/index.php

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

    return $response;
});
$app->get('/hoge', function (Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, hoge\n");

    return $response;
});

hoge、を追加。
……やっぱりNULLか。違うのか。
ちと、ここの深追いは一旦、止めておこう。


あとは直前のdispatchRouterAndPrepareRoute()が軽く気になるので、軽い目に閲覧。
ちと不明なところが多いので、一旦、まず体当たり。

    protected function dispatchRouterAndPrepareRoute(ServerRequestInterface $request, RouterInterface $router)
    {
        $routeInfo = $router->dispatch($request);
var_dump($routeInfo);

        if ($routeInfo[0] === Dispatcher::FOUND) {
echo "into if\n";
            $routeArguments = [];
            foreach ($routeInfo[2] as $k => $v) {
                $routeArguments[$k] = urldecode($v);
            }
var_dump($routeArguments);

            $route = $router->lookupRoute($routeInfo[1]);
            $route->prepare($request, $routeArguments);

            // add route to the request's attributes in case a middleware or handler needs access to the route
            $request = $request->withAttribute('route', $route);
        }

        $routeInfo['request'] = [$request->getMethod(), (string) $request->getUri()];
var_dump($routeInfo);
exit;

        return $request->withAttribute('routeInfo', $routeInfo);
    }

array(3) {
[0]=>
int(1)
[1]=>
string(6) "route0"
[2]=>
array(1) {
["name"]=>
string(4) "gallu"
}
}
into if
array(1) {
["name"]=>
string(4) "gallu"
}
array(4) {
[0]=>
int(1)
[1]=>
string(6) "route0"
[2]=>
array(1) {
["name"]=>
string(4) "gallu"
}
["request"]=>
array(2) {
[0]=>
string(3) "GET"
[1]=>
string(35) "http://dev.example.com:8080/hello/gallu"
}
}

ふむ……そこそこ予想込みなんだけど
・「$router->dispatch($request);」で、ヒットするrouteを取ってくる
・routeArgumentsに引数をパッキングしてる(多分、実働する関数の第三引数に使う用途じゃないかなぁ)
・$routeInfoのkey: 'request'に、メソッド名と実際のURIを付け足す
・requestのAttributeに「routeInfo」を付けたインスタンスをcloseして渡す(closeしているのはwithAttribute()の中)
って感じか。


だとすると
・ヒットしないとき
・二個以上ヒットするとき
の動きがそれぞれ興味あるなぁ。実験。


public/index.php

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

    return $response;
});
$app->get('/hoge', function (Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, hoge\n");

    return $response;
});
$app->get('/hoge', function (Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, hoge2\n");

    return $response;
});

まずは重複パターン。
「Slim Application Error」ををすげぇがっつりエラーw

Message: Cannot register two routes matching "/hoge" for method "GET"
File: /home/gallu/slim_test_plain/vendor/nikic/fast-route/src/DataGenerator/RegexBasedAbstract.php
Line: 86
Trace: #0 /home/gallu/slim_test_plain/vendor/nikic/fast-route/src/DataGenerator/RegexBasedAbstract.php(30): FastRoute\DataGenerator\RegexBasedAbstract->addStaticRoute('GET', Array, 'route2')
#1 /home/gallu/slim_test_plain/vendor/nikic/fast-route/src/RouteCollector.php(44): FastRoute\DataGenerator\RegexBasedAbstract->addRoute('GET', Array, 'route2')
#2 /home/gallu/slim_test_plain/vendor/slim/slim/Slim/Router.php(227): FastRoute\RouteCollector->addRoute(Array, '/hoge', 'route2')
#3 /home/gallu/slim_test_plain/vendor/nikic/fast-route/src/functions.php(25): Slim\Router->Slim\{closure}(Object(FastRoute\RouteCollector))
#4 /home/gallu/slim_test_plain/vendor/slim/slim/Slim/Router.php(238): FastRoute\simpleDispatcher(Object(Closure), Array)
#5 /home/gallu/slim_test_plain/vendor/slim/slim/Slim/Router.php(191): Slim\Router->createDispatcher()
#6 /home/gallu/slim_test_plain/vendor/slim/slim/Slim/App.php(582): Slim\Router->dispatch(Object(Slim\Http\Request))
#7 /home/gallu/slim_test_plain/vendor/slim/slim/Slim/App.php(506): Slim\App->dispatchRouterAndPrepareRoute(Object(Slim\Http\Request), Object(Slim\Router))
#8 /home/gallu/slim_test_plain/vendor/slim/slim/Slim/MiddlewareAwareTrait.php(117): Slim\App->__invoke(Object(Slim\Http\Request), Object(Slim\Http\Response))
#9 /home/gallu/slim_test_plain/vendor/slim/slim/Slim/App.php(405): Slim\App->callMiddlewareStack(Object(Slim\Http\Request), Object(Slim\Http\Response))
#10 /home/gallu/slim_test_plain/vendor/slim/slim/Slim/App.php(314): Slim\App->process(Object(Slim\Http\Request), Object(Slim\Http\Response))
#11 /home/gallu/slim_test_plain/public/index.php(24): Slim\App->run()
#12 {main}
View in rendered output by enabling the "displayErrorDetails" setting.

ふむ
vendor/nikic/fast-route/src/DataGenerator/RegexBasedAbstract.php

    private function addStaticRoute($httpMethod, $routeData, $handler)
    {
        $routeStr = $routeData[0];

        if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
            throw new BadRouteException(sprintf(
                'Cannot register two routes matching "%s" for method "%s"',
                $routeStr, $httpMethod
            ));
        }

ここか。
とりあえずガードされている事がわかったので安心。


なかった場合。

array(1) {
[0]=>
int(0)
}
array(2) {
[0]=>
int(0)
["request"]=>
array(2) {
[0]=>
string(3) "GET"
[1]=>
string(28) "http://dev.example.com:8080/foo"
}
}

うんrouteが空のままなのね。了解。


んで、改めて次。
vendor/slim/slim/Slim/App.php
__invoke()の

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

この辺。
先に気になったんでDispatcher::FOUNDを確認

int(1)

ほぉ。
なんとなし「isset($routeInfo[1])」でもいいんじゃね? とか思わなくもないけど、まぁ気にしない。………ダメか。「あるかないか」以外のstatusもあるぽいし。
気になったのが「Dispatcher::METHOD_NOT_ALLOWED」。
ちなみに値そのものは「int(2)」。
methodだしなぁ、で、可能性。


public/index.php

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

    return $response;
});
$app->post('/hoge', function (Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, hoge\n");

    return $response;
});

vendor/slim/slim/Slim/App.php

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

Trap 1
Trap 3
object(Slim\Handlers\NotAllowed)#40 (1) {
["knownContentTypes":protected]=>
array(4) {
[0]=>
string(16) "application/json"
[1]=>
string(15) "application/xml"
[2]=>
string(8) "text/xml"
[3]=>
string(9) "text/html"
}
}

ふむ。
・$this->container->has('notAllowedHandler')、は、デフォルトで作ってたと思うから、まぁ普通はtrue
・Slim\Handlers\NotAllowedクラスをcall
てな感じか。


軽く Slim\Handlers\NotAllowed を確認。
vendor/slim/slim/Slim/Handlers/NotAllowed.php
ふむ………renderHtmlNotAllowedMessage()にHTMLが直書きしたるのか。
まぁ普通に納得。どうにかしたきゃ
・継承してrenderHtmlNotAllowedMessage()メソッド上書き
・containerに突っ込みなおし
でいいだろうし。


ついでに
vendor/slim/slim/Slim/Handlers/NotFound.php
うん大体、同じ作りだわかりやすいwww


本題に戻して。
URIルーティング、あるけどmethodちげぇよ、な処理
は見えた。
その次。見つからないときパターンだね。

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

大まかには
・$this->container->has('notFoundHandler') ないならデフォのNotFoundException。でもまぁあるよねぇ
・コンテナにあるんならそいつ使う。 vendor/slim/slim/Slim/Handlers/NotFound.php だべさ
って感じ。


さて、全ての枝葉を切り取って、本丸中の本丸

        if ($routeInfo[0] === Dispatcher::FOUND) {
            $route = $router->lookupRoute($routeInfo[1]);
            return $route->run($request, $response);
        } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) {

ここの、trueの処理。
とりあえず
$route
$route->run
が気になるので引っ張り出してみる。
$routeはまぁ当然のごとく「object(Slim\Route)」。
んでは、runメソッドを紐解いてみませう。


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

あらシンプル。

    public function finalize()
    {
        if ($this->finalized) {
            return;
        }

        $groupMiddleware = [];
        foreach ($this->getGroups() as $group) {
            $groupMiddleware = array_merge($group->getMiddleware(), $groupMiddleware);
        }

        $this->middleware = array_merge($this->middleware, $groupMiddleware);

        foreach ($this->getMiddleware() as $middleware) {
            $this->addMiddleware($middleware);
        }

        $this->finalized = true;
    }

ふむ………ということは。

var_dump( $this->finalized );
        $this->finalize();
var_dump( $this->finalized );
exit;

bool(false)
bool(true)

うん予想通り。「二重に通るのを防いでる」感じか。
初期値

    private $finalized = false;

こんなんだしね。


middlewareは、多分今は設定してないからなぁ。

        $groupMiddleware = [];
        foreach ($this->getGroups() as $group) {
echo "groupMiddleware\n";
            $groupMiddleware = array_merge($group->getMiddleware(), $groupMiddleware);
        }

        $this->middleware = array_merge($this->middleware, $groupMiddleware);

        foreach ($this->getMiddleware() as $middleware) {
echo "addMiddleware\n";
            $this->addMiddleware($middleware);
        }
exit;

うん出力されない。
まぁここは一旦放置、で。


戻って、run()。

        // Traverse middleware stack and fetch updated response
        return $this->callMiddlewareStack($request, $response);

ここ、か。
………あら、ない。
継承元? use?

    use MiddlewareAwareTrait;


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

あったあった。
………あぁなんか見たなぁこれ。ここに戻ってくるのか。
ふむ………実験………の前に、プレ実験。

$obj = new stdClass();
$obj2 = new stdClass();

var_dump( spl_object_hash($obj), $obj );
var_dump( spl_object_hash($obj2), $obj2 );

string(32) "000000003df3aa3d0000000044a939ab"
object(stdClass)#1 (0) {
}
string(32) "000000003df3aa3e0000000044a939ab"
object(stdClass)#2 (0) {
}

悪い、とは言わんが、ちょっと「軽く一目で比較する」には、面倒だなぁ。上述だって「32文字中、31文字が一緒」とか、何の間違い探しだよw

$obj = new stdClass();
$obj2 = new stdClass();

var_dump( spl_object_id($obj), $obj );
var_dump( spl_object_id($obj2), $obj2 );

int(1)
object(stdClass)#1 (0) {
}
int(2)
object(stdClass)#2 (0) {
}

うんこっちのほうが閲覧性よいねぇ。
ただ、なんでか、spl_object_idって「(PHP 7 >= 7.2.0)」なので、ご注意のほどを。
普通に、5.3系くらいから居座ってもよさそうなもんだろうにねぇ。


で、本題に戻して。
callMiddlewareStackが「どーゆー風に(誰から、どんな$thisの文脈で)」callされてるかを確認。


vendor/slim/slim/Slim/MiddlewareAwareTrait.php

    public function callMiddlewareStack(ServerRequestInterface $request, ResponseInterface $response)
    {
var_dump( get_class($this) );
var_dump( spl_object_id($this) );

string(8) "Slim\App"
int(3)
string(10) "Slim\Route"
int(21)

まぁobject_id取るまでもなかったよねぇってのは置いといて。
・初回はApp
・次回はRoute
から呼ばれてる。何となし
・初回は「全体」から「1つのRouteを選ぶ」ために流れて
・次回は「処理の本丸を流す」ために流れる
のかなぁ?
なんで共通にしているのか、は、謎。


まぁ、心おきなく「Routeの__invoke」を見てみませう。
………さすがに長くなってきたので、次回。